From 162d0f951a53240ea2920e1c4efc6649829464a2 Mon Sep 17 00:00:00 2001 From: imstar15 Date: Sat, 7 Sep 2024 16:50:42 +0800 Subject: [PATCH] Run cargo fmt --all --- .../runtime-templates/simple/src/lib.rs | 36 +- pallets/automation-price/src/benchmarking.rs | 564 ++- pallets/automation-price/src/fees.rs | 233 +- pallets/automation-price/src/lib.rs | 3025 +++++++++-------- pallets/automation-price/src/mock.rs | 709 ++-- pallets/automation-price/src/tests.rs | 2945 ++++++++-------- pallets/automation-price/src/trigger.rs | 58 +- pallets/automation-price/src/types.rs | 50 +- 8 files changed, 3851 insertions(+), 3769 deletions(-) diff --git a/container-chains/runtime-templates/simple/src/lib.rs b/container-chains/runtime-templates/simple/src/lib.rs index babdf2d1d..d2b7e204f 100644 --- a/container-chains/runtime-templates/simple/src/lib.rs +++ b/container-chains/runtime-templates/simple/src/lib.rs @@ -842,24 +842,24 @@ impl pallet_automation_time::Config for Runtime { } impl pallet_automation_price::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type MaxTasksPerSlot = ConstU32<1>; - type MaxTasksPerAccount = ConstU32<32>; - type MaxTasksOverall = ConstU32<16_384>; - type MaxBlockWeight = MaxBlockWeight; - type MaxWeightPercentage = MaxWeightPercentage; - type WeightInfo = pallet_automation_price::weights::SubstrateWeight; - type ExecutionWeightFee = ExecutionWeightFee; - type Currency = Balances; - type MultiCurrency = Currencies; - type CurrencyId = TokenId; - type XcmpTransactor = XcmpHandler; - type EnsureProxy = AutomationEnsureProxy; - type CurrencyIdConvert = TokenIdConvert; - type FeeConversionRateProvider = FeePerSecondProvider; - type FeeHandler = pallet_automation_price::FeeHandler; - type UniversalLocation = UniversalLocation; - type SelfParaId = parachain_info::Pallet; + type RuntimeEvent = RuntimeEvent; + type MaxTasksPerSlot = ConstU32<1>; + type MaxTasksPerAccount = ConstU32<32>; + type MaxTasksOverall = ConstU32<16_384>; + type MaxBlockWeight = MaxBlockWeight; + type MaxWeightPercentage = MaxWeightPercentage; + type WeightInfo = pallet_automation_price::weights::SubstrateWeight; + type ExecutionWeightFee = ExecutionWeightFee; + type Currency = Balances; + type MultiCurrency = Currencies; + type CurrencyId = TokenId; + type XcmpTransactor = XcmpHandler; + type EnsureProxy = AutomationEnsureProxy; + type CurrencyIdConvert = TokenIdConvert; + type FeeConversionRateProvider = FeePerSecondProvider; + type FeeHandler = pallet_automation_price::FeeHandler; + type UniversalLocation = UniversalLocation; + type SelfParaId = parachain_info::Pallet; } impl_tanssi_pallets_config!(Runtime); diff --git a/pallets/automation-price/src/benchmarking.rs b/pallets/automation-price/src/benchmarking.rs index ac67213b3..5bb52d78f 100644 --- a/pallets/automation-price/src/benchmarking.rs +++ b/pallets/automation-price/src/benchmarking.rs @@ -27,8 +27,8 @@ use sp_runtime::traits::{AccountIdConversion, Saturating}; use staging_xcm::latest::prelude::*; use crate::{ - pallet::{Task, TaskId}, - Config, Pallet as AutomationPrice, + pallet::{Task, TaskId}, + Config, Pallet as AutomationPrice, }; const SEED: u32 = 0; @@ -46,299 +46,295 @@ const DECIMAL: u8 = 10_u8; // a helper function to prepare asset when setting up tasks or price because asset needs to be // defined before updating price fn setup_asset(authorized_wallets: Vec) { - let _ = AutomationPrice::::initialize_asset( - RawOrigin::Root.into(), - CHAIN.to_vec(), - EXCHANGE.to_vec(), - ASSET_TUR.to_vec(), - ASSET_USD.to_vec(), - DECIMAL, - authorized_wallets, - ); + let _ = AutomationPrice::::initialize_asset( + RawOrigin::Root.into(), + CHAIN.to_vec(), + EXCHANGE.to_vec(), + ASSET_TUR.to_vec(), + ASSET_USD.to_vec(), + DECIMAL, + authorized_wallets, + ); } // a helper method to schedule task with a set of default params to support benchmark easier -fn schedule_xcmp_task( - para_id: u32, - owner: T::AccountId, - call: Vec, -) { - let _ = AutomationPrice::::schedule_xcmp_task( - RawOrigin::Signed(owner).into(), - CHAIN.to_vec(), - EXCHANGE.to_vec(), - ASSET_TUR.to_vec(), - ASSET_USD.to_vec(), - 6000u128, - "gt".as_bytes().to_vec(), - vec![2000], - Box::new(Location::new(1, Parachain(para_id)).into()), - Box::new(Location::default().into()), - Box::new(AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: 0, - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0), - ); +fn schedule_xcmp_task(para_id: u32, owner: T::AccountId, call: Vec) { + let _ = AutomationPrice::::schedule_xcmp_task( + RawOrigin::Signed(owner).into(), + CHAIN.to_vec(), + EXCHANGE.to_vec(), + ASSET_TUR.to_vec(), + ASSET_USD.to_vec(), + 6000u128, + "gt".as_bytes().to_vec(), + vec![2000], + Box::new(Location::new(1, Parachain(para_id)).into()), + Box::new(Location::default().into()), + Box::new(AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: 0, + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0), + ); } // direct_task_schedule push the task directly to the task registry and relevant setup, // by pass the normal extrinsic execution. // This funciton should be used to prepare data for benchmark fn direct_task_schedule( - creator: T::AccountId, - task_id: TaskId, - expired_at: u128, - trigger_function: Vec, - price_target: u128, - encoded_call: Vec, + creator: T::AccountId, + task_id: TaskId, + expired_at: u128, + trigger_function: Vec, + price_target: u128, + encoded_call: Vec, ) -> Result<(), Error> { - let para_id: u32 = 2000; - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: 0, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - let schedule_as = account("caller", 0, SEED); - - let action = Action::XCMP { - destination, - schedule_fee, - execution_fee, - encoded_call, - encoded_call_weight, - overall_weight, - schedule_as: Some(schedule_as), - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }; - - let task: Task = Task:: { - owner_id: creator, - task_id, - chain: CHAIN.to_vec(), - exchange: EXCHANGE.to_vec(), - asset_pair: (ASSET_TUR.to_vec(), ASSET_USD.to_vec()), - expired_at, - trigger_function, - trigger_params: vec![price_target], - action, - }; - - AutomationPrice::::validate_and_schedule_task(task) + let para_id: u32 = 2000; + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: 0, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + let schedule_as = account("caller", 0, SEED); + + let action = Action::XCMP { + destination, + schedule_fee, + execution_fee, + encoded_call, + encoded_call_weight, + overall_weight, + schedule_as: Some(schedule_as), + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }; + + let task: Task = Task:: { + owner_id: creator, + task_id, + chain: CHAIN.to_vec(), + exchange: EXCHANGE.to_vec(), + asset_pair: (ASSET_TUR.to_vec(), ASSET_USD.to_vec()), + expired_at, + trigger_function, + trigger_params: vec![price_target], + action, + }; + + AutomationPrice::::validate_and_schedule_task(task) } benchmarks! { - initialize_asset_extrinsic { - let v in 1..5; - let asset_pair = (ASSET_TUR.to_vec(), ASSET_USD.to_vec()); - - let mut authorized_wallets: Vec = vec![]; - for i in 1..=v { - authorized_wallets.push(account("caller", i, SEED)); - } - } : { - let _ = AutomationPrice::::initialize_asset( - RawOrigin::Root.into(), - CHAIN.to_vec(), EXCHANGE.to_vec(), - ASSET_TUR.to_vec(), ASSET_USD.to_vec(), DECIMAL, authorized_wallets); - } - - asset_price_update_extrinsic { - // Depend on the size of the input, the weight change, ideally scale linearly - // Therefore we can simulate v from 1..100 and substrate will agg those value - let v in 1..100; - let sender : T::AccountId = account("caller", 0, SEED); - - setup_asset::(vec![sender.clone()]); - - let mut chains: Vec> = vec![]; - let mut exchanges: Vec> = vec![]; - let mut assets1: Vec> = vec![]; - let mut assets2: Vec> = vec![]; - let mut prices: Vec = vec![]; - let mut submitted_ats: Vec = vec![]; - let mut rounds: Vec = vec![]; - - for i in 1..=v { - chains.push(format!("CHAIN:{:?}", i).as_bytes().to_vec()); - exchanges.push(format!("EXCHANGE:{:?}", i).as_bytes().to_vec()); - assets1.push(format!("ASSET1{:?}", i).as_bytes().to_vec()); - assets2.push(format!("ASSET2{:?}", i).as_bytes().to_vec()); - prices.push(i as u128); - submitted_ats.push(i as u128); - rounds.push(i as u128); - } - } : { - let _ = AutomationPrice::::update_asset_prices( - RawOrigin::Signed(sender.clone()).into(), - chains, - exchanges, - assets1, - assets2, - prices, - submitted_ats, - rounds - ); - } - - schedule_xcmp_task_extrinsic { - let sender : T::AccountId = account("caller", 0, SEED); - let para_id: u32 = 1000; - let call: Vec = vec![2, 4, 5]; - setup_asset::(vec![sender.clone()]); - let transfer_amount = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); - let _ = T::Currency::deposit_creating( - &sender, - transfer_amount.saturating_mul(DEPOSIT_MULTIPLIER.into()), - ); - - } : { - schedule_xcmp_task::(para_id, sender, call); - } - - cancel_task_extrinsic { - let creator : T::AccountId = account("caller", 0, SEED); - let para_id: u32 = 1000; - let call: Vec = vec![2, 4, 5]; - setup_asset::(vec![creator.clone()]); - let transfer_amount = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); - let _ = T::Currency::deposit_creating( - &creator, - transfer_amount.saturating_mul(DEPOSIT_MULTIPLIER.into()), - ); - - // Schedule 10000 Task, This is just an arbitrary number to simular a big task registry - // Because of using StoragMap, and avoid dealing with vector - // our task look up will always be O(1) for time - let mut task_ids: Vec = vec![]; - for i in 1..100 { - // Fund the account so we can schedule task - let account_min = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); - let _ = T::Currency::deposit_creating(&creator, account_min.saturating_mul(DEPOSIT_MULTIPLIER.into())); - let _ = direct_task_schedule::(creator.clone(), format!("{:?}", i).as_bytes().to_vec(), i, "gt".as_bytes().to_vec(), i, vec![100, 200, (i % 256) as u8]); - task_ids.push(format!("{:?}", i).as_bytes().to_vec()); - } - - let task_id_to_cancel = "1".as_bytes().to_vec(); - } : { - let _ = AutomationPrice::::cancel_task(RawOrigin::Signed(creator).into(), task_id_to_cancel.clone()); - } - verify { - } - - run_xcmp_task { - let creator: T::AccountId = account("caller", 0, SEED); - let para_id: u32 = 2001; - let call = vec![4,5,6]; - - let local_para_id: u32 = 2114; - let destination = Location::new(1, Parachain(para_id)); - let local_sovereign_account: T::AccountId = Sibling::from(local_para_id).into_account_truncating(); - let _ = T::Currency::deposit_creating( - &local_sovereign_account, - T::Currency::minimum_balance().saturating_mul(DEPOSIT_MULTIPLIER.into()), - ); - - let fee = AssetPayment { asset_location: Location::new(1, Parachain(para_id)).into(), amount: 1000u128 }; - }: { - AutomationPrice::::run_xcmp_task(destination, creator, fee, call, Weight::from_parts(100_000, 0), Weight::from_parts(200_000, 0), InstructionSequence::PayThroughSovereignAccount) - } - - remove_task { - let creator : T::AccountId = account("caller", 0, SEED); - let para_id: u32 = 1000; - let call: Vec = vec![2, 4, 5]; - setup_asset::(vec![creator.clone()]); - let transfer_amount = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); - let _ = T::Currency::deposit_creating( - &creator, - transfer_amount.saturating_mul(DEPOSIT_MULTIPLIER.into()), - ); - - let para_id: u32 = 2000; - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: 0, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - let schedule_as: T::AccountId = account("caller", 0, SEED); - - // Schedule 10000 Task, This is just an arbitrary number to simular a big task registry - // Because of using StoragMap, and avoid dealing with vector - // our task look up will always be O(1) for time - let mut task_ids: Vec = vec![]; - let mut tasks: Vec> = vec![]; - for i in 1..100 { - let task_id = format!("{:?}", i).as_bytes().to_vec(); - let expired_at = i; - let trigger_function = "gt".as_bytes().to_vec(); - let price_target: u128 = i; - let encoded_call = vec![100, 200, (i % 256) as u8]; - - task_ids.push(format!("{:?}", i).as_bytes().to_vec()); - let action = Action::XCMP { - destination: destination.clone(), - schedule_fee: schedule_fee.clone(), - execution_fee: execution_fee.clone(), - encoded_call, - encoded_call_weight, - overall_weight, - schedule_as: Some(schedule_as.clone()), - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }; - - let task: Task = Task:: { - owner_id: creator.clone(), - task_id: task_id.clone(), - chain: CHAIN.to_vec(), - exchange: EXCHANGE.to_vec(), - asset_pair: (ASSET_TUR.to_vec(), ASSET_USD.to_vec()), - expired_at, - trigger_function, - trigger_params: vec![price_target], - action, - }; - let _ = AutomationPrice::::validate_and_schedule_task(task.clone()); - tasks.push(task); - } - - let task = tasks.pop().unwrap(); - }: { - // remove a task at the end to simulate the worst case - AutomationPrice::::remove_task(&task, Some(crate::Event::::TaskSweep { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: crate::TaskCondition::AlreadyExpired { - expired_at: task.expired_at, - now: 100, - } - })); - } - - - emit_event { - let owner_id: T::AccountId = account("call", 1, SEED); - let schedule_as: T::AccountId = account("schedule_as", 1, SEED); - let task_id: TaskId = vec![1,2,3]; - } : { - AutomationPrice::::deposit_event(crate::Event::::TaskScheduled { - owner_id, - task_id, - schedule_as: Some(schedule_as), - }); - } - - impl_benchmark_test_suite!( - AutomationPrice, - crate::mock::new_test_ext(crate::tests::START_BLOCK_TIME), - crate::mock::Test - ) + initialize_asset_extrinsic { + let v in 1..5; + let asset_pair = (ASSET_TUR.to_vec(), ASSET_USD.to_vec()); + + let mut authorized_wallets: Vec = vec![]; + for i in 1..=v { + authorized_wallets.push(account("caller", i, SEED)); + } + } : { + let _ = AutomationPrice::::initialize_asset( + RawOrigin::Root.into(), + CHAIN.to_vec(), EXCHANGE.to_vec(), + ASSET_TUR.to_vec(), ASSET_USD.to_vec(), DECIMAL, authorized_wallets); + } + + asset_price_update_extrinsic { + // Depend on the size of the input, the weight change, ideally scale linearly + // Therefore we can simulate v from 1..100 and substrate will agg those value + let v in 1..100; + let sender : T::AccountId = account("caller", 0, SEED); + + setup_asset::(vec![sender.clone()]); + + let mut chains: Vec> = vec![]; + let mut exchanges: Vec> = vec![]; + let mut assets1: Vec> = vec![]; + let mut assets2: Vec> = vec![]; + let mut prices: Vec = vec![]; + let mut submitted_ats: Vec = vec![]; + let mut rounds: Vec = vec![]; + + for i in 1..=v { + chains.push(format!("CHAIN:{:?}", i).as_bytes().to_vec()); + exchanges.push(format!("EXCHANGE:{:?}", i).as_bytes().to_vec()); + assets1.push(format!("ASSET1{:?}", i).as_bytes().to_vec()); + assets2.push(format!("ASSET2{:?}", i).as_bytes().to_vec()); + prices.push(i as u128); + submitted_ats.push(i as u128); + rounds.push(i as u128); + } + } : { + let _ = AutomationPrice::::update_asset_prices( + RawOrigin::Signed(sender.clone()).into(), + chains, + exchanges, + assets1, + assets2, + prices, + submitted_ats, + rounds + ); + } + + schedule_xcmp_task_extrinsic { + let sender : T::AccountId = account("caller", 0, SEED); + let para_id: u32 = 1000; + let call: Vec = vec![2, 4, 5]; + setup_asset::(vec![sender.clone()]); + let transfer_amount = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); + let _ = T::Currency::deposit_creating( + &sender, + transfer_amount.saturating_mul(DEPOSIT_MULTIPLIER.into()), + ); + + } : { + schedule_xcmp_task::(para_id, sender, call); + } + + cancel_task_extrinsic { + let creator : T::AccountId = account("caller", 0, SEED); + let para_id: u32 = 1000; + let call: Vec = vec![2, 4, 5]; + setup_asset::(vec![creator.clone()]); + let transfer_amount = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); + let _ = T::Currency::deposit_creating( + &creator, + transfer_amount.saturating_mul(DEPOSIT_MULTIPLIER.into()), + ); + + // Schedule 10000 Task, This is just an arbitrary number to simular a big task registry + // Because of using StoragMap, and avoid dealing with vector + // our task look up will always be O(1) for time + let mut task_ids: Vec = vec![]; + for i in 1..100 { + // Fund the account so we can schedule task + let account_min = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); + let _ = T::Currency::deposit_creating(&creator, account_min.saturating_mul(DEPOSIT_MULTIPLIER.into())); + let _ = direct_task_schedule::(creator.clone(), format!("{:?}", i).as_bytes().to_vec(), i, "gt".as_bytes().to_vec(), i, vec![100, 200, (i % 256) as u8]); + task_ids.push(format!("{:?}", i).as_bytes().to_vec()); + } + + let task_id_to_cancel = "1".as_bytes().to_vec(); + } : { + let _ = AutomationPrice::::cancel_task(RawOrigin::Signed(creator).into(), task_id_to_cancel.clone()); + } + verify { + } + + run_xcmp_task { + let creator: T::AccountId = account("caller", 0, SEED); + let para_id: u32 = 2001; + let call = vec![4,5,6]; + + let local_para_id: u32 = 2114; + let destination = Location::new(1, Parachain(para_id)); + let local_sovereign_account: T::AccountId = Sibling::from(local_para_id).into_account_truncating(); + let _ = T::Currency::deposit_creating( + &local_sovereign_account, + T::Currency::minimum_balance().saturating_mul(DEPOSIT_MULTIPLIER.into()), + ); + + let fee = AssetPayment { asset_location: Location::new(1, Parachain(para_id)).into(), amount: 1000u128 }; + }: { + AutomationPrice::::run_xcmp_task(destination, creator, fee, call, Weight::from_parts(100_000, 0), Weight::from_parts(200_000, 0), InstructionSequence::PayThroughSovereignAccount) + } + + remove_task { + let creator : T::AccountId = account("caller", 0, SEED); + let para_id: u32 = 1000; + let call: Vec = vec![2, 4, 5]; + setup_asset::(vec![creator.clone()]); + let transfer_amount = T::Currency::minimum_balance().saturating_mul(ED_MULTIPLIER.into()); + let _ = T::Currency::deposit_creating( + &creator, + transfer_amount.saturating_mul(DEPOSIT_MULTIPLIER.into()), + ); + + let para_id: u32 = 2000; + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: 0, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + let schedule_as: T::AccountId = account("caller", 0, SEED); + + // Schedule 10000 Task, This is just an arbitrary number to simular a big task registry + // Because of using StoragMap, and avoid dealing with vector + // our task look up will always be O(1) for time + let mut task_ids: Vec = vec![]; + let mut tasks: Vec> = vec![]; + for i in 1..100 { + let task_id = format!("{:?}", i).as_bytes().to_vec(); + let expired_at = i; + let trigger_function = "gt".as_bytes().to_vec(); + let price_target: u128 = i; + let encoded_call = vec![100, 200, (i % 256) as u8]; + + task_ids.push(format!("{:?}", i).as_bytes().to_vec()); + let action = Action::XCMP { + destination: destination.clone(), + schedule_fee: schedule_fee.clone(), + execution_fee: execution_fee.clone(), + encoded_call, + encoded_call_weight, + overall_weight, + schedule_as: Some(schedule_as.clone()), + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }; + + let task: Task = Task:: { + owner_id: creator.clone(), + task_id: task_id.clone(), + chain: CHAIN.to_vec(), + exchange: EXCHANGE.to_vec(), + asset_pair: (ASSET_TUR.to_vec(), ASSET_USD.to_vec()), + expired_at, + trigger_function, + trigger_params: vec![price_target], + action, + }; + let _ = AutomationPrice::::validate_and_schedule_task(task.clone()); + tasks.push(task); + } + + let task = tasks.pop().unwrap(); + }: { + // remove a task at the end to simulate the worst case + AutomationPrice::::remove_task(&task, Some(crate::Event::::TaskSweep { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: crate::TaskCondition::AlreadyExpired { + expired_at: task.expired_at, + now: 100, + } + })); + } + + + emit_event { + let owner_id: T::AccountId = account("call", 1, SEED); + let schedule_as: T::AccountId = account("schedule_as", 1, SEED); + let task_id: TaskId = vec![1,2,3]; + } : { + AutomationPrice::::deposit_event(crate::Event::::TaskScheduled { + owner_id, + task_id, + schedule_as: Some(schedule_as), + }); + } + + impl_benchmark_test_suite!( + AutomationPrice, + crate::mock::new_test_ext(crate::tests::START_BLOCK_TIME), + crate::mock::Test + ) } diff --git a/pallets/automation-price/src/fees.rs b/pallets/automation-price/src/fees.rs index 501d5a896..ebb859065 100644 --- a/pallets/automation-price/src/fees.rs +++ b/pallets/automation-price/src/fees.rs @@ -21,9 +21,9 @@ use crate::{AccountOf, Action, ActionOf, Config, Error, MultiBalanceOf, Pallet}; use orml_traits::MultiCurrency; use pallet_xcmp_handler::{InstructionSequence, XcmpTransactor}; use sp_runtime::{ - traits::{CheckedSub, Convert, Saturating, Zero}, - DispatchError, DispatchResult, SaturatedConversion, - TokenError::BelowMinimum, + traits::{CheckedSub, Convert, Saturating, Zero}, + DispatchError, DispatchResult, SaturatedConversion, + TokenError::BelowMinimum, }; use sp_std::marker::PhantomData; use staging_xcm::latest::prelude::*; @@ -31,125 +31,134 @@ use staging_xcm_builder::TakeRevenue; /// Handle execution fee payments in the context of automation actions pub trait HandleFees { - fn pay_checked_fees_for Result>( - owner: &AccountOf, - action: &ActionOf, - prereq: F, - ) -> Result; + fn pay_checked_fees_for Result>( + owner: &AccountOf, + action: &ActionOf, + prereq: F, + ) -> Result; } pub struct FeeHandler { - owner: T::AccountId, - pub schedule_fee_location: Location, - pub schedule_fee_amount: MultiBalanceOf, - pub execution_fee_amount: MultiBalanceOf, - _phantom_data: PhantomData, + owner: T::AccountId, + pub schedule_fee_location: Location, + pub schedule_fee_amount: MultiBalanceOf, + pub execution_fee_amount: MultiBalanceOf, + _phantom_data: PhantomData, } impl HandleFees for FeeHandler where - T: Config, - TR: TakeRevenue, + T: Config, + TR: TakeRevenue, { - fn pay_checked_fees_for Result>( - owner: &AccountOf, - action: &ActionOf, - prereq: F, - ) -> Result { - let fee_handler = Self::new(owner, action)?; - fee_handler.can_pay_fee().map_err(|_| Error::::InsufficientBalance)?; - let outcome = prereq()?; - fee_handler.pay_fees()?; - Ok(outcome) - } + fn pay_checked_fees_for Result>( + owner: &AccountOf, + action: &ActionOf, + prereq: F, + ) -> Result { + let fee_handler = Self::new(owner, action)?; + fee_handler + .can_pay_fee() + .map_err(|_| Error::::InsufficientBalance)?; + let outcome = prereq()?; + fee_handler.pay_fees()?; + Ok(outcome) + } } impl FeeHandler where - T: Config, - TR: TakeRevenue, + T: Config, + TR: TakeRevenue, { - /// Ensure the fee can be paid. - fn can_pay_fee(&self) -> Result<(), DispatchError> { - let fee = self.schedule_fee_amount.saturating_add(self.execution_fee_amount); - - if fee.is_zero() { - return Ok(()); - } - - // Manually check for ExistenceRequirement since MultiCurrency doesn't currently support it - let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee_location.clone()) - .ok_or("IncoveribleLocation")?; - let currency_id = currency_id.into(); - let free_balance = T::MultiCurrency::free_balance(currency_id, &self.owner); - - free_balance - .checked_sub(&fee) - .ok_or(DispatchError::Token(BelowMinimum))? - .checked_sub(&T::MultiCurrency::minimum_balance(currency_id)) - .ok_or(DispatchError::Token(BelowMinimum))?; - T::MultiCurrency::ensure_can_withdraw(currency_id, &self.owner, fee)?; - Ok(()) - } - - /// Withdraw the fee. - fn withdraw_fee(&self) -> Result<(), DispatchError> { - let fee = self.schedule_fee_amount.saturating_add(self.execution_fee_amount); - - if fee.is_zero() { - return Ok(()); - } - - let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee_location.clone()) - .ok_or("IncoveribleLocation")?; - - match T::MultiCurrency::withdraw(currency_id.into(), &self.owner, fee) { - Ok(_) => { - TR::take_revenue(Asset { - id: self.schedule_fee_location.clone().into(), - fun: Fungibility::Fungible(self.schedule_fee_amount.saturated_into()), - }); - - if self.execution_fee_amount > MultiBalanceOf::::zero() { - T::XcmpTransactor::pay_xcm_fee( - currency_id, - self.owner.clone(), - self.execution_fee_amount.saturated_into(), - )?; - } - - Ok(()) - }, - Err(_) => Err(DispatchError::Token(BelowMinimum)), - } - } - - /// Builds an instance of the struct - pub fn new(owner: &AccountOf, action: &ActionOf) -> Result { - let schedule_fee_location = action.schedule_fee_location::(); - - let schedule_fee_amount: u128 = - Pallet::::calculate_schedule_fee_amount(action)?.saturated_into(); - - let execution_fee_amount = match action.clone() { - Action::XCMP { execution_fee, instruction_sequence: InstructionSequence::PayThroughSovereignAccount, .. } => { - execution_fee.amount.saturated_into() - }, - _ => 0u32.saturated_into(), - }; - - Ok(Self { - owner: owner.clone(), - schedule_fee_location, - schedule_fee_amount: schedule_fee_amount.saturated_into(), - execution_fee_amount, - _phantom_data: Default::default(), - }) - } - - /// Executes the fee handler - fn pay_fees(self) -> DispatchResult { - // This should never error if can_pay_fee passed. - self.withdraw_fee().map_err(|_| Error::::LiquidityRestrictions)?; - Ok(()) - } + /// Ensure the fee can be paid. + fn can_pay_fee(&self) -> Result<(), DispatchError> { + let fee = self + .schedule_fee_amount + .saturating_add(self.execution_fee_amount); + + if fee.is_zero() { + return Ok(()); + } + + // Manually check for ExistenceRequirement since MultiCurrency doesn't currently support it + let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee_location.clone()) + .ok_or("IncoveribleLocation")?; + let currency_id = currency_id.into(); + let free_balance = T::MultiCurrency::free_balance(currency_id, &self.owner); + + free_balance + .checked_sub(&fee) + .ok_or(DispatchError::Token(BelowMinimum))? + .checked_sub(&T::MultiCurrency::minimum_balance(currency_id)) + .ok_or(DispatchError::Token(BelowMinimum))?; + T::MultiCurrency::ensure_can_withdraw(currency_id, &self.owner, fee)?; + Ok(()) + } + + /// Withdraw the fee. + fn withdraw_fee(&self) -> Result<(), DispatchError> { + let fee = self + .schedule_fee_amount + .saturating_add(self.execution_fee_amount); + + if fee.is_zero() { + return Ok(()); + } + + let currency_id = T::CurrencyIdConvert::convert(self.schedule_fee_location.clone()) + .ok_or("IncoveribleLocation")?; + + match T::MultiCurrency::withdraw(currency_id.into(), &self.owner, fee) { + Ok(_) => { + TR::take_revenue(Asset { + id: self.schedule_fee_location.clone().into(), + fun: Fungibility::Fungible(self.schedule_fee_amount.saturated_into()), + }); + + if self.execution_fee_amount > MultiBalanceOf::::zero() { + T::XcmpTransactor::pay_xcm_fee( + currency_id, + self.owner.clone(), + self.execution_fee_amount.saturated_into(), + )?; + } + + Ok(()) + } + Err(_) => Err(DispatchError::Token(BelowMinimum)), + } + } + + /// Builds an instance of the struct + pub fn new(owner: &AccountOf, action: &ActionOf) -> Result { + let schedule_fee_location = action.schedule_fee_location::(); + + let schedule_fee_amount: u128 = + Pallet::::calculate_schedule_fee_amount(action)?.saturated_into(); + + let execution_fee_amount = match action.clone() { + Action::XCMP { + execution_fee, + instruction_sequence: InstructionSequence::PayThroughSovereignAccount, + .. + } => execution_fee.amount.saturated_into(), + _ => 0u32.saturated_into(), + }; + + Ok(Self { + owner: owner.clone(), + schedule_fee_location, + schedule_fee_amount: schedule_fee_amount.saturated_into(), + execution_fee_amount, + _phantom_data: Default::default(), + }) + } + + /// Executes the fee handler + fn pay_fees(self) -> DispatchResult { + // This should never error if can_pay_fee passed. + self.withdraw_fee() + .map_err(|_| Error::::LiquidityRestrictions)?; + Ok(()) + } } diff --git a/pallets/automation-price/src/lib.rs b/pallets/automation-price/src/lib.rs index 5dc2d585d..a7eb93bfb 100644 --- a/pallets/automation-price/src/lib.rs +++ b/pallets/automation-price/src/lib.rs @@ -48,28 +48,33 @@ mod benchmarking; pub use fees::*; -use parity_scale_codec::Decode; use core::convert::{TryFrom, TryInto}; use cumulus_primitives_core::InteriorLocation; +use parity_scale_codec::Decode; use cumulus_primitives_core::ParaId; use frame_support::{ - pallet_prelude::{Encode, Get, Member, MaxEncodedLen, StorageVersion, Twox64Concat, StorageValue, DispatchResult, DispatchError, MaybeSerializeDeserialize, IsType, StorageNMap, NMapKey, StorageDoubleMap, ValueQuery, StorageMap, Parameter}, - traits::Currency, transactional, - weights::constants::WEIGHT_REF_TIME_PER_SECOND, + pallet_prelude::{ + DispatchError, DispatchResult, Encode, Get, IsType, MaxEncodedLen, + MaybeSerializeDeserialize, Member, NMapKey, Parameter, StorageDoubleMap, StorageMap, + StorageNMap, StorageValue, StorageVersion, Twox64Concat, ValueQuery, + }, + traits::Currency, + transactional, + weights::constants::WEIGHT_REF_TIME_PER_SECOND, }; use frame_system::pallet_prelude::*; use orml_traits::{FixedConversionRateProvider, MultiCurrency}; use pallet_timestamp::{self as timestamp}; use scale_info::{prelude::format, TypeInfo}; use sp_runtime::{ - traits::{CheckedConversion, Convert, SaturatedConversion, Saturating}, - ArithmeticError, Perbill, + traits::{CheckedConversion, Convert, SaturatedConversion, Saturating}, + ArithmeticError, Perbill, }; use sp_std::{boxed::Box, collections::btree_map::BTreeMap, ops::Bound::Included, vec, vec::Vec}; -pub use pallet_xcmp_handler::InstructionSequence; use ava_protocol_primitives::EnsureProxy; +pub use pallet_xcmp_handler::InstructionSequence; pub use weights::WeightInfo; use pallet_xcmp_handler::XcmpTransactor; @@ -77,1493 +82,1521 @@ use staging_xcm::{latest::prelude::*, VersionedLocation}; #[frame_support::pallet] pub mod pallet { - use super::*; - - pub type AccountOf = ::AccountId; - pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; - pub type MultiBalanceOf = <::MultiCurrency as MultiCurrency< - ::AccountId, - >>::Balance; - pub type ActionOf = Action>; - - pub type MultiCurrencyId = <::MultiCurrency as MultiCurrency< - ::AccountId, - >>::CurrencyId; - - type UnixTime = u64; - pub type TaskId = Vec; - pub type TaskAddress = (AccountOf, TaskId); - pub type TaskIdList = Vec>; - - type ChainName = Vec; - type Exchange = Vec; - - type AssetName = Vec; - type AssetPair = (AssetName, AssetName); - type AssetPrice = u128; - type TriggerFunction = Vec; - - /// The struct that stores all information needed for a task. - #[derive(Debug, Eq, Encode, Decode, TypeInfo, Clone)] - #[scale_info(skip_type_params(T))] - pub struct Task { - // origin data from the account schedule the tasks - pub owner_id: AccountOf, - - // generated data - pub task_id: TaskId, - - // user input data - pub chain: ChainName, - pub exchange: Exchange, - pub asset_pair: AssetPair, - pub expired_at: u128, - - // TODO: Maybe expose enum? - pub trigger_function: Vec, - pub trigger_params: Vec, - pub action: ActionOf, - } - - /// Needed for assert_eq to compare Tasks in tests due to BoundedVec. - impl PartialEq for Task { - fn eq(&self, other: &Self) -> bool { - // TODO: correct this - self.owner_id == other.owner_id - && self.task_id == other.task_id - && self.asset_pair == other.asset_pair - && self.trigger_function == other.trigger_function - && self.trigger_params == other.trigger_params - } - } - - #[pallet::config] - pub trait Config: frame_system::Config + pallet_timestamp::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - /// Weight information for the extrinsics in this module. - type WeightInfo: WeightInfo; - - /// The maximum number of tasks that can be scheduled for a time slot. - #[pallet::constant] - type MaxTasksPerSlot: Get; - - /// The maximum number of tasks that a single user can schedule - #[pallet::constant] - type MaxTasksPerAccount: Get; - - /// The maximum number of tasks across our entire system - #[pallet::constant] - type MaxTasksOverall: Get; - - /// The maximum weight per block. - #[pallet::constant] - type MaxBlockWeight: Get; - - /// The maximum percentage of weight per block used for scheduled tasks. - #[pallet::constant] - type MaxWeightPercentage: Get; - - #[pallet::constant] - type ExecutionWeightFee: Get>; - - /// The Currency type for interacting with balances - type Currency: Currency; - - /// The MultiCurrency type for interacting with balances - type MultiCurrency: MultiCurrency; - - /// The currencyIds that our chain supports. - type CurrencyId: Parameter - + Member - + Copy - + MaybeSerializeDeserialize - + Ord - + TypeInfo - + MaxEncodedLen - + From> - + Into> - + From; - - /// Converts CurrencyId to Multiloc - type CurrencyIdConvert: Convert> - + Convert>; - - /// Handler for fees - type FeeHandler: HandleFees; - - //type Origin: From<::RuntimeOrigin> - // + Into::Origin>>; - - /// Converts between comparable currencies - type FeeConversionRateProvider: FixedConversionRateProvider; - - /// This chain's Universal Location. - type UniversalLocation: Get; - - //The paraId of this chain. - type SelfParaId: Get; - - /// Utility for sending XCM messages - type XcmpTransactor: XcmpTransactor; - - /// Ensure proxy - type EnsureProxy: ava_protocol_primitives::EnsureProxy; - } - - const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); - - #[pallet::pallet] - #[pallet::without_storage_info] - #[pallet::storage_version(STORAGE_VERSION)] - pub struct Pallet(_); - - // TODO: Cleanup before merge - #[derive(Debug, Encode, Decode, TypeInfo)] - #[scale_info(skip_type_params(T))] - pub struct RegistryInfo { - round: u128, - decimal: u8, - last_update: u64, - oracle_providers: Vec>, - } - - // TODO: Use a ring buffer to also store last n history data effectively - #[derive(Debug, Encode, Decode, TypeInfo)] - #[scale_info(skip_type_params(T))] - pub struct PriceData { - pub round: u128, - pub updated_at: u128, - pub value: u128, - } - - // AssetRegistry holds information and metadata about the asset we support - #[pallet::storage] - #[pallet::getter(fn get_asset_registry_info)] - pub type AssetRegistry = StorageNMap< - _, - ( - NMapKey, - NMapKey, - NMapKey, - ), - RegistryInfo, - >; - - // PriceRegistry holds price only information for the asset we support - #[pallet::storage] - #[pallet::getter(fn get_asset_price_data)] - pub type PriceRegistry = StorageNMap< - _, - ( - NMapKey, - NMapKey, - NMapKey, - ), - PriceData, - >; - - // SortedTasksIndex is our sorted by price task shard - // Each task for a given asset is organized into a BTreeMap - // https://doc.rust-lang.org/std/collections/struct.BTreeMap.html#method.insert - // - key: Trigger Price - // - value: vector of task id - // TODO: move these to a trigger model - // TODO: handle task expiration - #[pallet::storage] - #[pallet::getter(fn get_sorted_tasks_index)] - pub type SortedTasksIndex = StorageNMap< - _, - ( - NMapKey, - NMapKey, - NMapKey, - NMapKey, - ), - BTreeMap>, - >; - - // SortedTasksByExpiration is our expiration sorted tasks - #[pallet::type_value] - pub fn DefaultSortedTasksByExpiration( - ) -> BTreeMap>> { - BTreeMap::>>::new() - } - #[pallet::storage] - #[pallet::getter(fn get_sorted_tasks_by_expiration)] - pub type SortedTasksByExpiration = StorageValue< - Value = BTreeMap>>, - QueryKind = ValueQuery, - OnEmpty = DefaultSortedTasksByExpiration, - >; - - // All active tasks, but organized by account - // In this storage, we only interested in returning task belong to an account, we also want to - // have fast lookup for task inserted/remove into the storage - // - // We also want to remove the expired task, so by leveraging this - #[pallet::storage] - #[pallet::getter(fn get_task)] - pub type Tasks = - StorageDoubleMap<_, Twox64Concat, AccountOf, Twox64Concat, TaskId, Task>; - - // Track various metric on our chain regarding tasks such as total task - // - #[pallet::storage] - #[pallet::getter(fn get_task_stat)] - pub type TaskStats = StorageMap<_, Twox64Concat, StatType, u64>; - - // Track various metric per account regarding tasks - // To count task per account, relying on Tasks storage alone mean we have to iterate overs - // value that share the first key (owner_id) to count. - // - // Store the task count - #[pallet::storage] - #[pallet::getter(fn get_account_stat)] - pub type AccountStats = - StorageDoubleMap<_, Twox64Concat, AccountOf, Twox64Concat, StatType, u64>; - - // TaskQueue stores the task to be executed. To run any tasks, they need to be move into this - // queue, from there our task execution pick it up and run it - // - // When task is run, we check the price once more and if it fall out of range, we move the task - // back to the Tasks Registry - // - // If the task is expired, we also won't run - #[pallet::storage] - #[pallet::getter(fn get_task_queue)] - pub type TaskQueue = StorageValue<_, TaskIdList, ValueQuery>; - - #[pallet::storage] - #[pallet::getter(fn is_shutdown)] - pub type Shutdown = StorageValue<_, bool, ValueQuery>; - - #[pallet::error] - pub enum Error { - InvalidTaskId, - /// Duplicate task - DuplicateTask, - - /// Non existent asset - AssetNotSupported, - AssetNotInitialized, - /// Asset already supported - AssetAlreadySupported, - AssetAlreadyInitialized, - /// Asset cannot be updated by this account - InvalidAssetSudo, - OracleNotAuthorized, - /// Asset must be in triggerable range. - AssetNotInTriggerableRange, - AssetUpdatePayloadMalform, - /// Block Time not set - BlockTimeNotSet, - /// Invalid Expiration Window for new asset - InvalidAssetExpirationWindow, - /// Maximum tasks reached for the slot - MaxTasksReached, - /// Maximum tasks reached for a given account - MaxTasksPerAccountReached, - /// Failed to insert task - TaskInsertionFailure, - /// Failed to remove task - TaskRemoveFailure, - /// Task Not Found When canceling - TaskNotFound, - /// Error when setting task expired less than the current block time - InvalidTaskExpiredAt, - /// Error when failed to update task expiration storage - TaskExpiredStorageFailedToUpdate, - /// Insufficient Balance - InsufficientBalance, - /// Restrictions on Liquidity in Account - LiquidityRestrictions, - /// Too Many Assets Created - AssetLimitReached, - - FeePaymentError, - CannotReanchor, - UnsupportedFeePayment, - /// The version of the `VersionedLocation` value used is not able - /// to be interpreted. - BadVersion, - } - - /// This is a event helper struct to help us making sense of the chain state and surrounded - /// environment state when we emit an event during task execution or task scheduling. - /// - /// They should contains enough information for an operator to look at and reason about "why do we - /// got here". - /// Many fields on this struct is optinal to support multiple error condition - #[derive(Debug, Encode, Eq, PartialEq, Decode, TypeInfo, Clone)] - pub enum TaskCondition { - TargetPriceMatched { - // record the state of the asset at the time the task is triggered - // when debugging we can use this to reason about why did the task is trigger - chain: ChainName, - exchange: Exchange, - asset_pair: AssetPair, - price: u128, - }, - AlreadyExpired { - // the original expired_at of this task - expired_at: u128, - // the block time when we emit this event. expired_at should always <= now - now: u128, - }, - - PriceAlreadyMoved { - chain: ChainName, - exchange: Exchange, - asset_pair: AssetPair, - price: u128, - - // The target price the task set - target_price: u128, - }, - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// Schedule task success. - TaskScheduled { - owner_id: AccountOf, - task_id: TaskId, - schedule_as: Option>, - }, - // an event when we're about to run the task - TaskTriggered { - owner_id: AccountOf, - task_id: TaskId, - condition: TaskCondition, - }, - // An event when the task ran succesfully - TaskExecuted { - owner_id: AccountOf, - task_id: TaskId, - }, - // An event when the task is trigger, ran but result in an error - TaskExecutionFailed { - owner_id: AccountOf, - task_id: TaskId, - error: DispatchError, - }, - // An event when the task is completed and removed from all of the queue - TaskCompleted { - owner_id: AccountOf, - task_id: TaskId, - }, - // An event when the task is cancelled, either by owner or by root - TaskCancelled { - owner_id: AccountOf, - task_id: TaskId, - }, - // An event whenever we expect a task but cannot find it - TaskNotFound { - owner_id: AccountOf, - task_id: TaskId, - }, - // An event when we are about to run task, but the task has expired right before - // it's actually run - TaskExpired { - owner_id: AccountOf, - task_id: TaskId, - condition: TaskCondition, - }, - // An event when we are proactively sweep expired task - // it's actually run - TaskSweep { - owner_id: AccountOf, - task_id: TaskId, - condition: TaskCondition, - }, - // An event happen in extreme case, where the chain is too busy, and there is pending task - // from previous block, and their respectively price has now moved against their matching - // target range - PriceAlreadyMoved { - owner_id: AccountOf, - task_id: TaskId, - condition: TaskCondition, - }, - AssetCreated { - chain: ChainName, - exchange: Exchange, - asset1: AssetName, - asset2: AssetName, - decimal: u8, - }, - AssetUpdated { - owner_id: AccountOf, - chain: ChainName, - exchange: Exchange, - asset1: AssetName, - asset2: AssetName, - price: u128, - }, - AssetDeleted { - chain: ChainName, - exchange: Exchange, - asset1: AssetName, - asset2: AssetName, - }, - } - - // #[pallet::hooks] - // impl Hooks> for Pallet { - // fn on_initialize(_: T::BlockNumber) -> Weight { - // if Self::is_shutdown() { - // return T::DbWeight::get().reads(1u64) - // } - - // let max_weight: Weight = Weight::from_parts( - // T::MaxWeightPercentage::get().mul_floor(T::MaxBlockWeight::get()), - // 0, - // ); - // Self::trigger_tasks(max_weight) - // } - - // fn on_idle(_: T::BlockNumber, remaining_weight: Weight) -> Weight { - // Self::sweep_expired_task(remaining_weight) - // } - // } - - #[pallet::call] - impl Pallet { - /// Initialize an asset - /// - /// Add a new asset - /// - /// # Parameters - /// * `asset`: asset type - /// * `target_price`: baseline price of the asset - /// * `upper_bound`: TBD - highest executable percentage increase for asset - /// * `lower_bound`: TBD - highest executable percentage decrease for asset - /// * `asset_owner`: owner of the asset - /// * `expiration_period`: how frequently the tasks for an asset should expire - /// - /// # Errors - #[pallet::call_index(1)] - #[pallet::weight(::WeightInfo::initialize_asset_extrinsic(asset_owners.len() as u32))] - #[transactional] - pub fn initialize_asset( - origin: OriginFor, - chain: Vec, - exchange: Vec, - asset1: AssetName, - asset2: AssetName, - decimal: u8, - asset_owners: Vec>, - ) -> DispatchResult { - // TODO: use sudo and remove this feature flag - // TODO: needs fees if opened up to non-sudo - // When enable dev-queue, we skip this check - #[cfg(not(feature = "dev-queue"))] - ensure_root(origin)?; - - Self::create_new_asset(chain, exchange, asset1, asset2, decimal, asset_owners)?; - - Ok(()) - } - - /// Update prices of multiple asset pairs at the same time - /// - /// Only authorized origin can update the price. The authorized origin is set when - /// initializing an asset. - /// - /// An asset is identified by this tuple: (chain, exchange, (asset1, asset2)). - /// - /// To support updating multiple pairs, each element of the tuple become a separate - /// argument to this function, where as each of these argument is a vector. - /// - /// Every element of each vector arguments, in the same position in the vector form the - /// above tuple. - /// - /// # Parameters - /// * `chains`: a vector of chain names - /// * `exchange`: a vector of exchange name - /// * `asset1`: a vector of asset1 name - /// * `asset2`: a vector of asset2 name - /// * `prices`: a vector of price of asset1, re-present in asset2 - /// * `submitted_at`: a vector of epoch. This epoch is the time when the price is recognized from the oracle provider - /// * `rounds`: a number to re-present which round of the asset price we're updating. Unused internally - #[pallet::call_index(2)] - #[pallet::weight(::WeightInfo::asset_price_update_extrinsic(assets1.len() as u32))] - #[transactional] - pub fn update_asset_prices( - origin: OriginFor, - chains: Vec, - exchanges: Vec, - assets1: Vec, - assets2: Vec, - prices: Vec, - submitted_at: Vec, - rounds: Vec, - ) -> DispatchResult { - let owner_id = ensure_signed(origin)?; - - let current_block_time = Self::get_current_block_time(); - if current_block_time.is_err() { - Err(Error::::BlockTimeNotSet)? - } - - let now = current_block_time.unwrap() as u128; - - if !(chains.len() == exchanges.len() - && exchanges.len() == assets1.len() - && assets1.len() == assets2.len() - && assets2.len() == prices.len() - && prices.len() == submitted_at.len() - && submitted_at.len() == rounds.len()) - { - Err(Error::::AssetUpdatePayloadMalform)? - } - - for (index, price) in prices.clone().iter().enumerate() { - let chain = chains[index].clone(); - let exchange = exchanges[index].clone(); - let asset1 = assets1[index].clone(); - let asset2 = assets2[index].clone(); - let round = rounds[index]; - - let key = (&chain, &exchange, (&asset1, &asset2)); - - if !AssetRegistry::::contains_key(&key) { - Err(Error::::AssetNotInitialized)? - } - - if let Some(asset_registry) = Self::get_asset_registry_info(key) { - let allow_wallets: Vec> = asset_registry.oracle_providers; - if !allow_wallets.contains(&owner_id) { - Err(Error::::OracleNotAuthorized)? - } - - // TODO: Eventually we will need to handle submitted_at and round properly when - // we had more than one oracle - // Currently not doing that check for the simplicity shake of interface - let this_round = match Self::get_asset_price_data(key) { - Some(previous_price) => previous_price.round + 1, - None => round, - }; - - PriceRegistry::::insert( - &key, - PriceData { round: this_round, updated_at: now, value: *price }, - ); - - Self::deposit_event(Event::AssetUpdated { - owner_id: owner_id.clone(), - chain, - exchange, - asset1, - asset2, - price: *price, - }); - } - } - Ok(()) - } - - /// Delete an asset. Delete may not happen immediately if there was task scheduled for - /// this asset. Upon - /// - /// # Parameters - /// * `asset`: asset type - /// * `directions`: number of directions of data input. (up, down, ?) - /// - /// # Errors - #[pallet::call_index(3)] - #[pallet::weight(::WeightInfo::initialize_asset_extrinsic(1))] - #[transactional] - pub fn delete_asset( - origin: OriginFor, - chain: ChainName, - exchange: Exchange, - asset1: AssetName, - asset2: AssetName, - ) -> DispatchResult { - // TODO: use sudo and remove this feature flag - // When enable dev queue, we want to skip this root check so local development can - // happen easier - #[cfg(not(feature = "dev-queue"))] - ensure_root(origin)?; - - let key = (&chain, &exchange, (&asset1, &asset2)); - if let Some(_asset_info) = Self::get_asset_registry_info(key) { - AssetRegistry::::remove(&key); - PriceRegistry::::remove(&key); - Self::deposit_event(Event::AssetDeleted { chain, exchange, asset1, asset2 }); - } else { - Err(Error::::AssetNotSupported)? - } - Ok(()) - } - - #[pallet::call_index(4)] - #[pallet::weight(::WeightInfo::schedule_xcmp_task_extrinsic())] - #[transactional] - pub fn schedule_xcmp_task( - origin: OriginFor, - chain: ChainName, - exchange: Exchange, - asset1: AssetName, - asset2: AssetName, - expired_at: u128, - trigger_function: Vec, - trigger_param: Vec, - destination: Box, - schedule_fee: Box, - execution_fee: Box, - encoded_call: Vec, - encoded_call_weight: Weight, - overall_weight: Weight, - ) -> DispatchResult { - // Step 1: - // Build Task and put it into the task registry - // Step 2: - // Put task id on the index - // TODO: the value to be inserted into the BTree should come from a function that - // extract value from param - // - // TODO: HANDLE FEE to see user can pay fee - let owner_id = ensure_signed(origin)?; - let task_id = Self::generate_task_id(); - - let destination = - Location::try_from(*destination).map_err(|()| Error::::BadVersion)?; - let schedule_fee = - Location::try_from(*schedule_fee).map_err(|()| Error::::BadVersion)?; - - let action = Action::XCMP { - destination, - schedule_fee, - execution_fee: *execution_fee, - encoded_call, - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughSovereignAccount, - }; - - let task: Task = Task:: { - owner_id, - task_id, - chain, - exchange, - asset_pair: (asset1, asset2), - expired_at, - trigger_function, - trigger_params: trigger_param, - action, - }; - - Self::validate_and_schedule_task(task)?; - Ok(()) - } - - /// Schedule a task through XCMP through proxy account to fire an XCMP message with a provided call. - /// - /// Before the task can be scheduled the task must past validation checks. - /// * The transaction is signed - /// * The asset pair is already initialized - /// - /// # Parameters - /// * `chain`: The chain name where we will send the task over - /// * `exchange`: the exchange name where we - /// * `asset1`: The payment asset location required for scheduling automation task. - /// * `asset2`: The fee will be paid for XCMP execution. - /// * `expired_at`: the epoch when after that time we will remove the task if it has not been executed yet - /// * `trigger_function`: currently only support `gt` or `lt`. Essentially mean greater than or less than. - /// * `trigger_params`: a list of parameter to feed into `trigger_function`. with `gt` and `lt` we only need to pass the target price as a single element vector - /// * `schedule_fee`: The payment asset location required for scheduling automation task. - /// * `execution_fee`: The fee will be paid for XCMP execution. - /// * `encoded_call`: Call that will be sent via XCMP to the parachain id provided. - /// * `encoded_call_weight`: Required weight at most the provided call will take. - /// * `overall_weight`: The overall weight in which fees will be paid for XCM instructions. - #[pallet::call_index(5)] - #[pallet::weight(::WeightInfo::schedule_xcmp_task_extrinsic().saturating_add(T::DbWeight::get().reads(1)))] - #[transactional] - pub fn schedule_xcmp_task_through_proxy( - origin: OriginFor, - chain: ChainName, - exchange: Exchange, - asset1: AssetName, - asset2: AssetName, - expired_at: u128, - trigger_function: Vec, - trigger_params: Vec, - - destination: Box, - schedule_fee: Box, - execution_fee: Box, - encoded_call: Vec, - encoded_call_weight: Weight, - overall_weight: Weight, - schedule_as: T::AccountId, - ) -> DispatchResult { - let owner_id = ensure_signed(origin)?; - - // Make sure the owner is the proxy account of the user account. - T::EnsureProxy::ensure_ok(schedule_as.clone(), owner_id.clone())?; - - let destination = - Location::try_from(*destination).map_err(|()| Error::::BadVersion)?; - let schedule_fee = - Location::try_from(*schedule_fee).map_err(|()| Error::::BadVersion)?; - - let action = Action::XCMP { - destination, - schedule_fee, - execution_fee: *execution_fee, - encoded_call, - encoded_call_weight, - overall_weight, - schedule_as: Some(schedule_as), - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }; - - let task_id = Self::generate_task_id(); - let task: Task = Task:: { - owner_id, - task_id, - chain, - exchange, - asset_pair: (asset1, asset2), - expired_at, - trigger_function, - trigger_params, - action, - }; - - Self::validate_and_schedule_task(task)?; - Ok(()) - } - - // When cancel task we remove it from: - // Task Registry - // SortedTasksIndex - // AccountTasks - // Task Queue: if the task is already on the queue but haven't got run yet, - // we will attemppt to remove it - #[pallet::call_index(6)] - #[pallet::weight(::WeightInfo::cancel_task_extrinsic())] - #[transactional] - pub fn cancel_task(origin: OriginFor, task_id: TaskId) -> DispatchResult { - let owner_id = ensure_signed(origin)?; - - if let Some(task) = Self::get_task(&owner_id, &task_id) { - Self::remove_task( - &task, - Some(Event::TaskCancelled { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - }), - ); - } else { - Err(Error::::TaskNotFound)? - } - - Ok(()) - } - } - - impl Pallet { - pub fn generate_task_id() -> TaskId { - let current_block_number = - TryInto::::try_into(>::block_number()) - .ok() - .unwrap_or(0); - - let tx_id = >::extrinsic_index().unwrap_or(0); - - let evt_index = >::event_count(); - - format!("{:}-{:}-{:}", current_block_number, tx_id, evt_index) - .as_bytes() - .to_vec() - } - - // Move task from the SortedTasksIndex into TaskQueue that are ready to be process - pub fn shift_tasks(max_weight: Weight) -> Weight { - let weight_left: Weight = max_weight; - - // TODO: Look into asset that has price move instead - let task_to_process: &mut TaskIdList = &mut Vec::new(); - - for key in SortedTasksIndex::::iter_keys() { - let (chain, exchange, asset_pair, trigger_func) = key.clone(); - - // TODO: Swap asset to check pair - let current_price_wrap = - Self::get_asset_price_data((&chain, &exchange, &asset_pair)); - - if current_price_wrap.is_none() { - continue; - }; - // Example: sell orders - // - // In the list we had tasks such as - // - task1: sell when price > 10 - // - task2: sell when price > 20 - // - task3: sell when price > 30 - // If price used to be 5, and now it's 15, task1 got run - // If price used to be 5, and now it's 25, task1 and task2 got run - // If price used to be 5, and now it's 35, all tasks are run - // - // Example: buy orders - // - // In the list we had tasks such as - // - task1: buy when price < 10 - // - task2: buy when price < 20 - // - task3: buy when price < 30 - // If price used to be 500, and now it's 25, task3 got run - // If price used to be 500, and now it's 15, task2 and task3 got run - // If price used to be 500, and now it's 5, all tasks are run - // - // TODO: handle atomic and transaction - if let Some(mut tasks) = Self::get_sorted_tasks_index(&key) { - let current_price = current_price_wrap.unwrap(); - - for (&price, task_ids) in - (tasks.clone()).range(range_by_trigger_func(&trigger_func, ¤t_price)) - { - // Remove because we map this into task queue - tasks.remove(&price); - let t = &mut (&mut (task_ids.clone())); - task_to_process.append(t); - } - - // all tasks are moved to process, delete the queue - if tasks.is_empty() { - SortedTasksIndex::::remove(&key); - } else { - SortedTasksIndex::::insert(&key, tasks); - } - } - } - - if !task_to_process.is_empty() { - if TaskQueue::::exists() { - let mut old_task = TaskQueue::::get(); - old_task.append(task_to_process); - TaskQueue::::put(old_task); - } else { - TaskQueue::::put(task_to_process); - }; - } - - weight_left - } - - /// Trigger tasks for the block time. - /// - /// Complete as many tasks as possible given the maximum weight. - pub fn trigger_tasks(max_weight: Weight) -> Weight { - let mut weight_left: Weight = max_weight; - let check_time_and_deletion_weight = T::DbWeight::get().reads(2u64); - if weight_left.ref_time() < check_time_and_deletion_weight.ref_time() { - return weight_left; - } - - Self::shift_tasks(weight_left); - - // Now we can run those tasks - // TODO: We need to calculate enough weight and balance the tasks so we won't be skew - // by a particular kind of task asset - // - // Now we run as much task as possible - // If weight is over, task will be picked up next time - // If the price is no longer matched, they will be put back into the TaskRegistry - let task_queue = Self::get_task_queue(); - - weight_left = weight_left - // for above read - .saturating_sub(T::DbWeight::get().reads(1u64)) - // For measuring the TaskQueue::::put(tasks_left); - .saturating_sub(T::DbWeight::get().writes(1u64)); - if !task_queue.is_empty() { - let (tasks_left, new_weight_left) = Self::run_tasks(task_queue, weight_left); - weight_left = new_weight_left; - TaskQueue::::put(tasks_left); - } - - weight_left - } - - pub fn create_new_asset( - chain: ChainName, - exchange: Exchange, - asset1: AssetName, - asset2: AssetName, - decimal: u8, - asset_owners: Vec>, - ) -> Result<(), DispatchError> { - let key = (&chain, &exchange, (&asset1, &asset2)); - - if AssetRegistry::::contains_key(&key) { - Err(Error::::AssetAlreadyInitialized)? - } - - let asset_info = RegistryInfo:: { - decimal, - round: 0, - last_update: 0, - oracle_providers: asset_owners, - }; - - AssetRegistry::::insert(key, asset_info); - - Self::deposit_event(Event::AssetCreated { chain, exchange, asset1, asset2, decimal }); - Ok(()) - } - - pub fn get_current_time_slot() -> Result> { - let now = >::get().saturated_into::(); - if now == 0 { - Err(Error::::BlockTimeNotSet)? - } - let now = now.saturating_div(1000); - let diff_to_min = now % 60; - Ok(now.saturating_sub(diff_to_min)) - } - - pub fn run_xcmp_task( - destination: Location, - caller: T::AccountId, - fee: AssetPayment, - encoded_call: Vec, - encoded_call_weight: Weight, - overall_weight: Weight, - flow: InstructionSequence, - ) -> (Weight, Option) { - let fee_asset_location = Location::try_from(fee.asset_location); - if fee_asset_location.is_err() { - return ( - ::WeightInfo::run_xcmp_task(), - Some(Error::::BadVersion.into()), - ); - } - let fee_asset_location = fee_asset_location.unwrap(); - - match T::XcmpTransactor::transact_xcm( - destination, - fee_asset_location, - fee.amount, - caller, - encoded_call, - encoded_call_weight, - overall_weight, - flow, - ) { - Ok(()) => (::WeightInfo::run_xcmp_task(), None), - Err(e) => (::WeightInfo::run_xcmp_task(), Some(e)), - } - } - - // return epoch time of current block - pub fn get_current_block_time() -> Result { - let now = >::get() - .checked_into::() - .ok_or(ArithmeticError::Overflow)?; - - if now == 0 { - Err(Error::::BlockTimeNotSet)?; - } - - let now = now.checked_div(1000).ok_or(ArithmeticError::Overflow)?; - Ok(now) - } - - // Check whether a task can run or not based on its expiration and price. - // - // A task can be queued but got expired when it's about to run, in that case, we don't want - // it to be run. - // - // Or the price might move by the time task is invoked, we don't want it to get run either. - fn task_can_run(task: &Task) -> (Option, Weight) { - let mut consumed_weight: Weight = Weight::zero(); - - // If we cannot extract time from the block, then somthing horrible wrong, let not move - // forward - let current_block_time = Self::get_current_block_time(); - if current_block_time.is_err() { - return (None, consumed_weight); - } - - let now = current_block_time.unwrap(); - - if task.expired_at < now.into() { - consumed_weight = - consumed_weight.saturating_add(::WeightInfo::emit_event()); - - Self::deposit_event(Event::TaskExpired { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: TaskCondition::AlreadyExpired { - expired_at: task.expired_at, - now: now.into(), - }, - }); - - return (None, consumed_weight); - } - - // read storage once to get the price - consumed_weight = consumed_weight.saturating_add(T::DbWeight::get().reads(1u64)); - if let Some(this_task_asset_price) = - Self::get_asset_price_data((&task.chain, &task.exchange, &task.asset_pair)) - { - if task.is_price_condition_match(&this_task_asset_price) { - return ( - Some(TaskCondition::TargetPriceMatched { - chain: task.chain.clone(), - exchange: task.exchange.clone(), - asset_pair: task.asset_pair.clone(), - price: this_task_asset_price.value, - }), - consumed_weight, - ); - } else { - Self::deposit_event(Event::PriceAlreadyMoved { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: TaskCondition::PriceAlreadyMoved { - chain: task.chain.clone(), - exchange: task.exchange.clone(), - asset_pair: task.asset_pair.clone(), - price: this_task_asset_price.value, - - target_price: task.trigger_params[0], - }, - }); - - return (None, consumed_weight); - } - } - - // This happen because we cannot find the price, so the task cannot be run - (None, consumed_weight) - } - - /// Runs as many tasks as the weight allows from the provided vec of task_ids. - /// - /// Returns a vec with the tasks that were not run and the remaining weight. - pub fn run_tasks( - mut task_ids: TaskIdList, - mut weight_left: Weight, - ) -> (TaskIdList, Weight) { - let mut consumed_task_index: usize = 0; - - // If we cannot extract time from the block, then somthing horrible wrong, let not move - // forward - let current_block_time = Self::get_current_block_time(); - if current_block_time.is_err() { - return (task_ids, weight_left); - } - - let _now = current_block_time.unwrap(); - - for (owner_id, task_id) in task_ids.iter() { - consumed_task_index.saturating_inc(); - - let action_weight = match Self::get_task(owner_id, task_id) { - None => { - Self::deposit_event(Event::TaskNotFound { - owner_id: owner_id.clone(), - task_id: task_id.clone(), - }); - ::WeightInfo::emit_event() - }, - Some(task) => { - let (task_condition, test_can_run_weight) = Self::task_can_run(&task); - - if task_condition.is_none() { - test_can_run_weight - } else { - Self::deposit_event(Event::TaskTriggered { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: task_condition.unwrap(), - }); - - let _total_task = - Self::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v); - let _total_task_per_account = Self::get_account_stat( - &task.owner_id, - StatType::TotalTasksPerAccount, - ) - .map_or(0, |v| v); - - let (task_action_weight, task_dispatch_error) = - match task.action.clone() { - Action::XCMP { - destination, - execution_fee, - schedule_as, - encoded_call, - encoded_call_weight, - overall_weight, - instruction_sequence, - .. - } => Self::run_xcmp_task( - destination, - schedule_as.unwrap_or(task.owner_id.clone()), - execution_fee, - encoded_call, - encoded_call_weight, - overall_weight, - instruction_sequence, - ), - }; - - Self::remove_task(&task, None); - - if let Some(err) = task_dispatch_error { - Self::deposit_event(Event::::TaskExecutionFailed { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - error: err, - }); - } else { - Self::deposit_event(Event::::TaskExecuted { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - }); - } - - Self::deposit_event(Event::::TaskCompleted { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - }); - - task_action_weight - .saturating_add(T::DbWeight::get().writes(1u64)) - .saturating_add(T::DbWeight::get().reads(1u64)) - } - }, - }; - - weight_left = weight_left.saturating_sub(action_weight); - - let run_another_task_weight = ::WeightInfo::emit_event() - .saturating_add(T::DbWeight::get().writes(1u64)) - .saturating_add(T::DbWeight::get().reads(1u64)); - if weight_left.ref_time() < run_another_task_weight.ref_time() { - break; - } - } - - if consumed_task_index == task_ids.len() { - (vec![], weight_left) - } else { - (task_ids.split_off(consumed_task_index), weight_left) - } - } - - // Handle task removal. There are a few places task need to be remove: - // - Tasks storage - // - TaskQueue if the task is already queued - // - TaskStats: decrease task count - // - AccountStats: decrease task count - // - SortedTasksIndex: sorted task by price - // - SortedTasksByExpiration: sorted task by expired epch - pub fn remove_task(task: &Task, event: Option>) { - Tasks::::remove(task.owner_id.clone(), task.task_id.clone()); - - // Remove it from SortedTasksIndex - let key = (&task.chain, &task.exchange, &task.asset_pair, &task.trigger_function); - if let Some(mut sorted_tasks_by_price) = Self::get_sorted_tasks_index(key) { - if let Some(tasks) = sorted_tasks_by_price.get_mut(&task.trigger_params[0]) { - if let Some(pos) = tasks.iter().position(|x| { - let (_, task_id) = x; - *task_id == task.task_id - }) { - tasks.remove(pos); - } - - if tasks.is_empty() { - // if there is no more task on this slot, clear it up - sorted_tasks_by_price.remove(&task.trigger_params[0].clone()); - } - SortedTasksIndex::::insert(&key, sorted_tasks_by_price); - } - } - - // Remove it from the SortedTasksByExpiration - SortedTasksByExpiration::::mutate(|sorted_tasks_by_expiration| { - if let Some(expired_task_slot) = - sorted_tasks_by_expiration.get_mut(&task.expired_at) - { - expired_task_slot.remove(&task.task_id); - if expired_task_slot.is_empty() { - sorted_tasks_by_expiration.remove(&task.expired_at); - } - } - }); - - // Update metrics - let total_task = Self::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v); - let total_task_per_account = - Self::get_account_stat(&task.owner_id, StatType::TotalTasksPerAccount) - .map_or(0, |v| v); - - if total_task >= 1 { - TaskStats::::insert(StatType::TotalTasksOverall, total_task - 1); - } - - if total_task_per_account >= 1 { - AccountStats::::insert( - task.owner_id.clone(), - StatType::TotalTasksPerAccount, - total_task_per_account - 1, - ); - } - - if let Some(e) = event { - Self::deposit_event(e); - } - } - - // Sweep as mucht ask we can and return the remaining weight - pub fn sweep_expired_task(remaining_weight: Weight) -> Weight { - if remaining_weight.ref_time() <= T::DbWeight::get().reads(1u64).ref_time() { - // Weight too low, not enough to do anything useful - return remaining_weight; - } - - let current_block_time = Self::get_current_block_time(); - - if current_block_time.is_err() { - // Cannot get time, this probably is the first block - return remaining_weight; - } - - let now = current_block_time.unwrap() as u128; - - // At the end we will most likely need to write back the updated storage, so here we - // account for that write - let mut unused_weight = remaining_weight - .saturating_sub(T::DbWeight::get().reads(1u64)) - .saturating_sub(T::DbWeight::get().writes(1u64)); - let mut tasks_by_expiration = Self::get_sorted_tasks_by_expiration(); - - let mut expired_shards: Vec = vec![]; - // Use Included(now) because if this task has not run at the end of this block, then - // that mean at next block it for sure will expired - 'outer: for (expired_time, task_ids) in - tasks_by_expiration.range_mut((Included(&0_u128), Included(&now))) - { - for (task_id, owner_id) in task_ids.iter() { - if unused_weight.ref_time() - > T::DbWeight::get() - .reads(1u64) - .saturating_add(::WeightInfo::remove_task()) - .ref_time() - { - unused_weight = unused_weight - .saturating_sub(T::DbWeight::get().reads(1u64)) - .saturating_sub(::WeightInfo::remove_task()); - - // Now let remove the task from chain storage - if let Some(task) = Self::get_task(owner_id, task_id) { - Self::remove_task( - &task, - Some(Event::TaskSweep { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: TaskCondition::AlreadyExpired { - expired_at: task.expired_at, - now, - }, - }), - ); - } - } else { - // If there is not enough weight left, break all the way out, we had - // already save one weight for the write to update storage back - break 'outer; - } - } - expired_shards.push(*expired_time); - } - - unused_weight - } - - // Task is write into a sorted storage, re-present by BTreeMap so we can find and expired them - pub fn track_expired_task(task: &Task) -> Result> { - // first we got back the reference to the underlying storage - // perform relevant update to write task to the right shard by expired - // time, then the value is store back to storage - let mut tasks_by_expiration = Self::get_sorted_tasks_by_expiration(); - - if let Some(task_shard) = tasks_by_expiration.get_mut(&task.expired_at) { - task_shard.insert(task.task_id.clone(), task.owner_id.clone()); - } else { - tasks_by_expiration.insert( - task.expired_at, - BTreeMap::from([(task.task_id.clone(), task.owner_id.clone())]), - ); - } - SortedTasksByExpiration::::put(tasks_by_expiration); - - Ok(true) - } - - /// With transaction will protect against a partial success where N of M execution times might be full, - /// rolling back any successful insertions into the schedule task table. - /// Validate and schedule task. - /// This will also charge the execution fee. - /// TODO: double check atomic - pub fn validate_and_schedule_task(task: Task) -> Result<(), Error> { - if task.task_id.is_empty() { - Err(Error::::InvalidTaskId)? - } - - let current_block_time = Self::get_current_block_time(); - if current_block_time.is_err() { - // Cannot get time, this probably is the first block - Err(Error::::BlockTimeNotSet)? - } - - let now = current_block_time.unwrap() as u128; - - if task.expired_at <= now { - Err(Error::::InvalidTaskExpiredAt)? - } - - let total_task = Self::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v); - let total_task_per_account = - Self::get_account_stat(&task.owner_id, StatType::TotalTasksPerAccount) - .map_or(0, |v| v); - - // check task total limit per account and overall - if total_task >= T::MaxTasksOverall::get().into() { - Err(Error::::MaxTasksReached)? - } - - // check task total limit per account and overall - if total_task_per_account >= T::MaxTasksPerAccount::get().into() { - Err(Error::::MaxTasksPerAccountReached)? - } - - match task.action.clone() { - Action::XCMP { execution_fee, instruction_sequence, .. } => { - let asset_location = Location::try_from(execution_fee.asset_location) - .map_err(|()| Error::::BadVersion)?; - let asset_location = asset_location - .reanchored( - &Location::new(1, Parachain(T::SelfParaId::get().into())), - &T::UniversalLocation::get(), - ) - .map_err(|_| Error::::CannotReanchor)?; - // Only native token are supported as the XCMP fee for local deductions - if instruction_sequence == InstructionSequence::PayThroughSovereignAccount - && asset_location != Location::new(0, Here) - { - Err(Error::::UnsupportedFeePayment)? - } - }, - }; - - let fee_result = T::FeeHandler::pay_checked_fees_for( - &(task.owner_id.clone()), - &(task.action.clone()), - || { - Tasks::::insert(task.owner_id.clone(), task.task_id.clone(), &task); - - // Post task processing, increase relevant metrics data - TaskStats::::insert(StatType::TotalTasksOverall, total_task + 1); - AccountStats::::insert( - task.owner_id.clone(), - StatType::TotalTasksPerAccount, - total_task_per_account + 1, - ); - - let key = - (&task.chain, &task.exchange, &task.asset_pair, &task.trigger_function); - - if let Some(mut sorted_task_index) = Self::get_sorted_tasks_index(key) { - // TODO: remove hard code and take right param - if let Some(tasks_by_price) = - sorted_task_index.get_mut(&(task.trigger_params[0])) - { - tasks_by_price.push((task.owner_id.clone(), task.task_id.clone())); - } else { - sorted_task_index.insert( - task.trigger_params[0], - vec![(task.owner_id.clone(), task.task_id.clone())], - ); - } - SortedTasksIndex::::insert(key, sorted_task_index); - } else { - let mut sorted_task_index = BTreeMap::>::new(); - sorted_task_index.insert( - task.trigger_params[0], - vec![(task.owner_id.clone(), task.task_id.clone())], - ); - - // TODO: sorted based on trigger_function comparison of the parameter - // then at the time of trigger we cut off all the left part of the tree - SortedTasksIndex::::insert(key, sorted_task_index); - }; - - Ok(()) - }, - ); - - if fee_result.is_err() { - Err(Error::::FeePaymentError)? - } - - if Self::track_expired_task(&task).is_err() { - Err(Error::::TaskExpiredStorageFailedToUpdate)? - } - - let schedule_as = match task.action.clone() { - Action::XCMP { schedule_as, .. } => schedule_as, - }; - - Self::deposit_event(Event::TaskScheduled { - owner_id: task.owner_id, - task_id: task.task_id, - schedule_as, - }); - Ok(()) - } - - /// Calculates the execution fee for a given action based on weight and num of executions - /// - /// Fee saturates at Weight/BalanceOf when there are an unreasonable num of executions - /// In practice, executions is bounded by T::MaxExecutionTimes and unlikely to saturate - pub fn calculate_schedule_fee_amount( - action: &ActionOf, - ) -> Result, DispatchError> { - let total_weight = action.execution_weight::()?; - - let schedule_fee_location = action.schedule_fee_location::(); - let schedule_fee_location = schedule_fee_location - .reanchored( - &Location::new(1, Parachain(T::SelfParaId::get().into())), - &T::UniversalLocation::get(), - ) - .map_err(|_| Error::::CannotReanchor)?; - - let fee = if schedule_fee_location == Location::default() { - T::ExecutionWeightFee::get() - .saturating_mul(>::saturated_from(total_weight)) - } else { - let raw_fee = - T::FeeConversionRateProvider::get_fee_per_second(&schedule_fee_location) - .ok_or("CouldNotDetermineFeePerSecond")? - .checked_mul(total_weight as u128) - .ok_or("FeeOverflow") - .map(|raw_fee| raw_fee / (WEIGHT_REF_TIME_PER_SECOND as u128))?; - >::saturated_from(raw_fee) - }; - - Ok(fee) - } - } + use super::*; + + pub type AccountOf = ::AccountId; + pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + pub type MultiBalanceOf = <::MultiCurrency as MultiCurrency< + ::AccountId, + >>::Balance; + pub type ActionOf = Action>; + + pub type MultiCurrencyId = <::MultiCurrency as MultiCurrency< + ::AccountId, + >>::CurrencyId; + + type UnixTime = u64; + pub type TaskId = Vec; + pub type TaskAddress = (AccountOf, TaskId); + pub type TaskIdList = Vec>; + + type ChainName = Vec; + type Exchange = Vec; + + type AssetName = Vec; + type AssetPair = (AssetName, AssetName); + type AssetPrice = u128; + type TriggerFunction = Vec; + + /// The struct that stores all information needed for a task. + #[derive(Debug, Eq, Encode, Decode, TypeInfo, Clone)] + #[scale_info(skip_type_params(T))] + pub struct Task { + // origin data from the account schedule the tasks + pub owner_id: AccountOf, + + // generated data + pub task_id: TaskId, + + // user input data + pub chain: ChainName, + pub exchange: Exchange, + pub asset_pair: AssetPair, + pub expired_at: u128, + + // TODO: Maybe expose enum? + pub trigger_function: Vec, + pub trigger_params: Vec, + pub action: ActionOf, + } + + /// Needed for assert_eq to compare Tasks in tests due to BoundedVec. + impl PartialEq for Task { + fn eq(&self, other: &Self) -> bool { + // TODO: correct this + self.owner_id == other.owner_id + && self.task_id == other.task_id + && self.asset_pair == other.asset_pair + && self.trigger_function == other.trigger_function + && self.trigger_params == other.trigger_params + } + } + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_timestamp::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Weight information for the extrinsics in this module. + type WeightInfo: WeightInfo; + + /// The maximum number of tasks that can be scheduled for a time slot. + #[pallet::constant] + type MaxTasksPerSlot: Get; + + /// The maximum number of tasks that a single user can schedule + #[pallet::constant] + type MaxTasksPerAccount: Get; + + /// The maximum number of tasks across our entire system + #[pallet::constant] + type MaxTasksOverall: Get; + + /// The maximum weight per block. + #[pallet::constant] + type MaxBlockWeight: Get; + + /// The maximum percentage of weight per block used for scheduled tasks. + #[pallet::constant] + type MaxWeightPercentage: Get; + + #[pallet::constant] + type ExecutionWeightFee: Get>; + + /// The Currency type for interacting with balances + type Currency: Currency; + + /// The MultiCurrency type for interacting with balances + type MultiCurrency: MultiCurrency; + + /// The currencyIds that our chain supports. + type CurrencyId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + Ord + + TypeInfo + + MaxEncodedLen + + From> + + Into> + + From; + + /// Converts CurrencyId to Multiloc + type CurrencyIdConvert: Convert> + + Convert>; + + /// Handler for fees + type FeeHandler: HandleFees; + + //type Origin: From<::RuntimeOrigin> + // + Into::Origin>>; + + /// Converts between comparable currencies + type FeeConversionRateProvider: FixedConversionRateProvider; + + /// This chain's Universal Location. + type UniversalLocation: Get; + + //The paraId of this chain. + type SelfParaId: Get; + + /// Utility for sending XCM messages + type XcmpTransactor: XcmpTransactor; + + /// Ensure proxy + type EnsureProxy: ava_protocol_primitives::EnsureProxy; + } + + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::without_storage_info] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + // TODO: Cleanup before merge + #[derive(Debug, Encode, Decode, TypeInfo)] + #[scale_info(skip_type_params(T))] + pub struct RegistryInfo { + round: u128, + decimal: u8, + last_update: u64, + oracle_providers: Vec>, + } + + // TODO: Use a ring buffer to also store last n history data effectively + #[derive(Debug, Encode, Decode, TypeInfo)] + #[scale_info(skip_type_params(T))] + pub struct PriceData { + pub round: u128, + pub updated_at: u128, + pub value: u128, + } + + // AssetRegistry holds information and metadata about the asset we support + #[pallet::storage] + #[pallet::getter(fn get_asset_registry_info)] + pub type AssetRegistry = StorageNMap< + _, + ( + NMapKey, + NMapKey, + NMapKey, + ), + RegistryInfo, + >; + + // PriceRegistry holds price only information for the asset we support + #[pallet::storage] + #[pallet::getter(fn get_asset_price_data)] + pub type PriceRegistry = StorageNMap< + _, + ( + NMapKey, + NMapKey, + NMapKey, + ), + PriceData, + >; + + // SortedTasksIndex is our sorted by price task shard + // Each task for a given asset is organized into a BTreeMap + // https://doc.rust-lang.org/std/collections/struct.BTreeMap.html#method.insert + // - key: Trigger Price + // - value: vector of task id + // TODO: move these to a trigger model + // TODO: handle task expiration + #[pallet::storage] + #[pallet::getter(fn get_sorted_tasks_index)] + pub type SortedTasksIndex = StorageNMap< + _, + ( + NMapKey, + NMapKey, + NMapKey, + NMapKey, + ), + BTreeMap>, + >; + + // SortedTasksByExpiration is our expiration sorted tasks + #[pallet::type_value] + pub fn DefaultSortedTasksByExpiration( + ) -> BTreeMap>> { + BTreeMap::>>::new() + } + #[pallet::storage] + #[pallet::getter(fn get_sorted_tasks_by_expiration)] + pub type SortedTasksByExpiration = StorageValue< + Value = BTreeMap>>, + QueryKind = ValueQuery, + OnEmpty = DefaultSortedTasksByExpiration, + >; + + // All active tasks, but organized by account + // In this storage, we only interested in returning task belong to an account, we also want to + // have fast lookup for task inserted/remove into the storage + // + // We also want to remove the expired task, so by leveraging this + #[pallet::storage] + #[pallet::getter(fn get_task)] + pub type Tasks = + StorageDoubleMap<_, Twox64Concat, AccountOf, Twox64Concat, TaskId, Task>; + + // Track various metric on our chain regarding tasks such as total task + // + #[pallet::storage] + #[pallet::getter(fn get_task_stat)] + pub type TaskStats = StorageMap<_, Twox64Concat, StatType, u64>; + + // Track various metric per account regarding tasks + // To count task per account, relying on Tasks storage alone mean we have to iterate overs + // value that share the first key (owner_id) to count. + // + // Store the task count + #[pallet::storage] + #[pallet::getter(fn get_account_stat)] + pub type AccountStats = + StorageDoubleMap<_, Twox64Concat, AccountOf, Twox64Concat, StatType, u64>; + + // TaskQueue stores the task to be executed. To run any tasks, they need to be move into this + // queue, from there our task execution pick it up and run it + // + // When task is run, we check the price once more and if it fall out of range, we move the task + // back to the Tasks Registry + // + // If the task is expired, we also won't run + #[pallet::storage] + #[pallet::getter(fn get_task_queue)] + pub type TaskQueue = StorageValue<_, TaskIdList, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn is_shutdown)] + pub type Shutdown = StorageValue<_, bool, ValueQuery>; + + #[pallet::error] + pub enum Error { + InvalidTaskId, + /// Duplicate task + DuplicateTask, + + /// Non existent asset + AssetNotSupported, + AssetNotInitialized, + /// Asset already supported + AssetAlreadySupported, + AssetAlreadyInitialized, + /// Asset cannot be updated by this account + InvalidAssetSudo, + OracleNotAuthorized, + /// Asset must be in triggerable range. + AssetNotInTriggerableRange, + AssetUpdatePayloadMalform, + /// Block Time not set + BlockTimeNotSet, + /// Invalid Expiration Window for new asset + InvalidAssetExpirationWindow, + /// Maximum tasks reached for the slot + MaxTasksReached, + /// Maximum tasks reached for a given account + MaxTasksPerAccountReached, + /// Failed to insert task + TaskInsertionFailure, + /// Failed to remove task + TaskRemoveFailure, + /// Task Not Found When canceling + TaskNotFound, + /// Error when setting task expired less than the current block time + InvalidTaskExpiredAt, + /// Error when failed to update task expiration storage + TaskExpiredStorageFailedToUpdate, + /// Insufficient Balance + InsufficientBalance, + /// Restrictions on Liquidity in Account + LiquidityRestrictions, + /// Too Many Assets Created + AssetLimitReached, + + FeePaymentError, + CannotReanchor, + UnsupportedFeePayment, + /// The version of the `VersionedLocation` value used is not able + /// to be interpreted. + BadVersion, + } + + /// This is a event helper struct to help us making sense of the chain state and surrounded + /// environment state when we emit an event during task execution or task scheduling. + /// + /// They should contains enough information for an operator to look at and reason about "why do we + /// got here". + /// Many fields on this struct is optinal to support multiple error condition + #[derive(Debug, Encode, Eq, PartialEq, Decode, TypeInfo, Clone)] + pub enum TaskCondition { + TargetPriceMatched { + // record the state of the asset at the time the task is triggered + // when debugging we can use this to reason about why did the task is trigger + chain: ChainName, + exchange: Exchange, + asset_pair: AssetPair, + price: u128, + }, + AlreadyExpired { + // the original expired_at of this task + expired_at: u128, + // the block time when we emit this event. expired_at should always <= now + now: u128, + }, + + PriceAlreadyMoved { + chain: ChainName, + exchange: Exchange, + asset_pair: AssetPair, + price: u128, + + // The target price the task set + target_price: u128, + }, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Schedule task success. + TaskScheduled { + owner_id: AccountOf, + task_id: TaskId, + schedule_as: Option>, + }, + // an event when we're about to run the task + TaskTriggered { + owner_id: AccountOf, + task_id: TaskId, + condition: TaskCondition, + }, + // An event when the task ran succesfully + TaskExecuted { + owner_id: AccountOf, + task_id: TaskId, + }, + // An event when the task is trigger, ran but result in an error + TaskExecutionFailed { + owner_id: AccountOf, + task_id: TaskId, + error: DispatchError, + }, + // An event when the task is completed and removed from all of the queue + TaskCompleted { + owner_id: AccountOf, + task_id: TaskId, + }, + // An event when the task is cancelled, either by owner or by root + TaskCancelled { + owner_id: AccountOf, + task_id: TaskId, + }, + // An event whenever we expect a task but cannot find it + TaskNotFound { + owner_id: AccountOf, + task_id: TaskId, + }, + // An event when we are about to run task, but the task has expired right before + // it's actually run + TaskExpired { + owner_id: AccountOf, + task_id: TaskId, + condition: TaskCondition, + }, + // An event when we are proactively sweep expired task + // it's actually run + TaskSweep { + owner_id: AccountOf, + task_id: TaskId, + condition: TaskCondition, + }, + // An event happen in extreme case, where the chain is too busy, and there is pending task + // from previous block, and their respectively price has now moved against their matching + // target range + PriceAlreadyMoved { + owner_id: AccountOf, + task_id: TaskId, + condition: TaskCondition, + }, + AssetCreated { + chain: ChainName, + exchange: Exchange, + asset1: AssetName, + asset2: AssetName, + decimal: u8, + }, + AssetUpdated { + owner_id: AccountOf, + chain: ChainName, + exchange: Exchange, + asset1: AssetName, + asset2: AssetName, + price: u128, + }, + AssetDeleted { + chain: ChainName, + exchange: Exchange, + asset1: AssetName, + asset2: AssetName, + }, + } + + // #[pallet::hooks] + // impl Hooks> for Pallet { + // fn on_initialize(_: T::BlockNumber) -> Weight { + // if Self::is_shutdown() { + // return T::DbWeight::get().reads(1u64) + // } + + // let max_weight: Weight = Weight::from_parts( + // T::MaxWeightPercentage::get().mul_floor(T::MaxBlockWeight::get()), + // 0, + // ); + // Self::trigger_tasks(max_weight) + // } + + // fn on_idle(_: T::BlockNumber, remaining_weight: Weight) -> Weight { + // Self::sweep_expired_task(remaining_weight) + // } + // } + + #[pallet::call] + impl Pallet { + /// Initialize an asset + /// + /// Add a new asset + /// + /// # Parameters + /// * `asset`: asset type + /// * `target_price`: baseline price of the asset + /// * `upper_bound`: TBD - highest executable percentage increase for asset + /// * `lower_bound`: TBD - highest executable percentage decrease for asset + /// * `asset_owner`: owner of the asset + /// * `expiration_period`: how frequently the tasks for an asset should expire + /// + /// # Errors + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::initialize_asset_extrinsic(asset_owners.len() as u32))] + #[transactional] + pub fn initialize_asset( + origin: OriginFor, + chain: Vec, + exchange: Vec, + asset1: AssetName, + asset2: AssetName, + decimal: u8, + asset_owners: Vec>, + ) -> DispatchResult { + // TODO: use sudo and remove this feature flag + // TODO: needs fees if opened up to non-sudo + // When enable dev-queue, we skip this check + #[cfg(not(feature = "dev-queue"))] + ensure_root(origin)?; + + Self::create_new_asset(chain, exchange, asset1, asset2, decimal, asset_owners)?; + + Ok(()) + } + + /// Update prices of multiple asset pairs at the same time + /// + /// Only authorized origin can update the price. The authorized origin is set when + /// initializing an asset. + /// + /// An asset is identified by this tuple: (chain, exchange, (asset1, asset2)). + /// + /// To support updating multiple pairs, each element of the tuple become a separate + /// argument to this function, where as each of these argument is a vector. + /// + /// Every element of each vector arguments, in the same position in the vector form the + /// above tuple. + /// + /// # Parameters + /// * `chains`: a vector of chain names + /// * `exchange`: a vector of exchange name + /// * `asset1`: a vector of asset1 name + /// * `asset2`: a vector of asset2 name + /// * `prices`: a vector of price of asset1, re-present in asset2 + /// * `submitted_at`: a vector of epoch. This epoch is the time when the price is recognized from the oracle provider + /// * `rounds`: a number to re-present which round of the asset price we're updating. Unused internally + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::asset_price_update_extrinsic(assets1.len() as u32))] + #[transactional] + pub fn update_asset_prices( + origin: OriginFor, + chains: Vec, + exchanges: Vec, + assets1: Vec, + assets2: Vec, + prices: Vec, + submitted_at: Vec, + rounds: Vec, + ) -> DispatchResult { + let owner_id = ensure_signed(origin)?; + + let current_block_time = Self::get_current_block_time(); + if current_block_time.is_err() { + Err(Error::::BlockTimeNotSet)? + } + + let now = current_block_time.unwrap() as u128; + + if !(chains.len() == exchanges.len() + && exchanges.len() == assets1.len() + && assets1.len() == assets2.len() + && assets2.len() == prices.len() + && prices.len() == submitted_at.len() + && submitted_at.len() == rounds.len()) + { + Err(Error::::AssetUpdatePayloadMalform)? + } + + for (index, price) in prices.clone().iter().enumerate() { + let chain = chains[index].clone(); + let exchange = exchanges[index].clone(); + let asset1 = assets1[index].clone(); + let asset2 = assets2[index].clone(); + let round = rounds[index]; + + let key = (&chain, &exchange, (&asset1, &asset2)); + + if !AssetRegistry::::contains_key(&key) { + Err(Error::::AssetNotInitialized)? + } + + if let Some(asset_registry) = Self::get_asset_registry_info(key) { + let allow_wallets: Vec> = asset_registry.oracle_providers; + if !allow_wallets.contains(&owner_id) { + Err(Error::::OracleNotAuthorized)? + } + + // TODO: Eventually we will need to handle submitted_at and round properly when + // we had more than one oracle + // Currently not doing that check for the simplicity shake of interface + let this_round = match Self::get_asset_price_data(key) { + Some(previous_price) => previous_price.round + 1, + None => round, + }; + + PriceRegistry::::insert( + &key, + PriceData { + round: this_round, + updated_at: now, + value: *price, + }, + ); + + Self::deposit_event(Event::AssetUpdated { + owner_id: owner_id.clone(), + chain, + exchange, + asset1, + asset2, + price: *price, + }); + } + } + Ok(()) + } + + /// Delete an asset. Delete may not happen immediately if there was task scheduled for + /// this asset. Upon + /// + /// # Parameters + /// * `asset`: asset type + /// * `directions`: number of directions of data input. (up, down, ?) + /// + /// # Errors + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::initialize_asset_extrinsic(1))] + #[transactional] + pub fn delete_asset( + origin: OriginFor, + chain: ChainName, + exchange: Exchange, + asset1: AssetName, + asset2: AssetName, + ) -> DispatchResult { + // TODO: use sudo and remove this feature flag + // When enable dev queue, we want to skip this root check so local development can + // happen easier + #[cfg(not(feature = "dev-queue"))] + ensure_root(origin)?; + + let key = (&chain, &exchange, (&asset1, &asset2)); + if let Some(_asset_info) = Self::get_asset_registry_info(key) { + AssetRegistry::::remove(&key); + PriceRegistry::::remove(&key); + Self::deposit_event(Event::AssetDeleted { + chain, + exchange, + asset1, + asset2, + }); + } else { + Err(Error::::AssetNotSupported)? + } + Ok(()) + } + + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::schedule_xcmp_task_extrinsic())] + #[transactional] + pub fn schedule_xcmp_task( + origin: OriginFor, + chain: ChainName, + exchange: Exchange, + asset1: AssetName, + asset2: AssetName, + expired_at: u128, + trigger_function: Vec, + trigger_param: Vec, + destination: Box, + schedule_fee: Box, + execution_fee: Box, + encoded_call: Vec, + encoded_call_weight: Weight, + overall_weight: Weight, + ) -> DispatchResult { + // Step 1: + // Build Task and put it into the task registry + // Step 2: + // Put task id on the index + // TODO: the value to be inserted into the BTree should come from a function that + // extract value from param + // + // TODO: HANDLE FEE to see user can pay fee + let owner_id = ensure_signed(origin)?; + let task_id = Self::generate_task_id(); + + let destination = + Location::try_from(*destination).map_err(|()| Error::::BadVersion)?; + let schedule_fee = + Location::try_from(*schedule_fee).map_err(|()| Error::::BadVersion)?; + + let action = Action::XCMP { + destination, + schedule_fee, + execution_fee: *execution_fee, + encoded_call, + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughSovereignAccount, + }; + + let task: Task = Task:: { + owner_id, + task_id, + chain, + exchange, + asset_pair: (asset1, asset2), + expired_at, + trigger_function, + trigger_params: trigger_param, + action, + }; + + Self::validate_and_schedule_task(task)?; + Ok(()) + } + + /// Schedule a task through XCMP through proxy account to fire an XCMP message with a provided call. + /// + /// Before the task can be scheduled the task must past validation checks. + /// * The transaction is signed + /// * The asset pair is already initialized + /// + /// # Parameters + /// * `chain`: The chain name where we will send the task over + /// * `exchange`: the exchange name where we + /// * `asset1`: The payment asset location required for scheduling automation task. + /// * `asset2`: The fee will be paid for XCMP execution. + /// * `expired_at`: the epoch when after that time we will remove the task if it has not been executed yet + /// * `trigger_function`: currently only support `gt` or `lt`. Essentially mean greater than or less than. + /// * `trigger_params`: a list of parameter to feed into `trigger_function`. with `gt` and `lt` we only need to pass the target price as a single element vector + /// * `schedule_fee`: The payment asset location required for scheduling automation task. + /// * `execution_fee`: The fee will be paid for XCMP execution. + /// * `encoded_call`: Call that will be sent via XCMP to the parachain id provided. + /// * `encoded_call_weight`: Required weight at most the provided call will take. + /// * `overall_weight`: The overall weight in which fees will be paid for XCM instructions. + #[pallet::call_index(5)] + #[pallet::weight(::WeightInfo::schedule_xcmp_task_extrinsic().saturating_add(T::DbWeight::get().reads(1)))] + #[transactional] + pub fn schedule_xcmp_task_through_proxy( + origin: OriginFor, + chain: ChainName, + exchange: Exchange, + asset1: AssetName, + asset2: AssetName, + expired_at: u128, + trigger_function: Vec, + trigger_params: Vec, + + destination: Box, + schedule_fee: Box, + execution_fee: Box, + encoded_call: Vec, + encoded_call_weight: Weight, + overall_weight: Weight, + schedule_as: T::AccountId, + ) -> DispatchResult { + let owner_id = ensure_signed(origin)?; + + // Make sure the owner is the proxy account of the user account. + T::EnsureProxy::ensure_ok(schedule_as.clone(), owner_id.clone())?; + + let destination = + Location::try_from(*destination).map_err(|()| Error::::BadVersion)?; + let schedule_fee = + Location::try_from(*schedule_fee).map_err(|()| Error::::BadVersion)?; + + let action = Action::XCMP { + destination, + schedule_fee, + execution_fee: *execution_fee, + encoded_call, + encoded_call_weight, + overall_weight, + schedule_as: Some(schedule_as), + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }; + + let task_id = Self::generate_task_id(); + let task: Task = Task:: { + owner_id, + task_id, + chain, + exchange, + asset_pair: (asset1, asset2), + expired_at, + trigger_function, + trigger_params, + action, + }; + + Self::validate_and_schedule_task(task)?; + Ok(()) + } + + // When cancel task we remove it from: + // Task Registry + // SortedTasksIndex + // AccountTasks + // Task Queue: if the task is already on the queue but haven't got run yet, + // we will attemppt to remove it + #[pallet::call_index(6)] + #[pallet::weight(::WeightInfo::cancel_task_extrinsic())] + #[transactional] + pub fn cancel_task(origin: OriginFor, task_id: TaskId) -> DispatchResult { + let owner_id = ensure_signed(origin)?; + + if let Some(task) = Self::get_task(&owner_id, &task_id) { + Self::remove_task( + &task, + Some(Event::TaskCancelled { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + }), + ); + } else { + Err(Error::::TaskNotFound)? + } + + Ok(()) + } + } + + impl Pallet { + pub fn generate_task_id() -> TaskId { + let current_block_number = + TryInto::::try_into(>::block_number()) + .ok() + .unwrap_or(0); + + let tx_id = >::extrinsic_index().unwrap_or(0); + + let evt_index = >::event_count(); + + format!("{:}-{:}-{:}", current_block_number, tx_id, evt_index) + .as_bytes() + .to_vec() + } + + // Move task from the SortedTasksIndex into TaskQueue that are ready to be process + pub fn shift_tasks(max_weight: Weight) -> Weight { + let weight_left: Weight = max_weight; + + // TODO: Look into asset that has price move instead + let task_to_process: &mut TaskIdList = &mut Vec::new(); + + for key in SortedTasksIndex::::iter_keys() { + let (chain, exchange, asset_pair, trigger_func) = key.clone(); + + // TODO: Swap asset to check pair + let current_price_wrap = + Self::get_asset_price_data((&chain, &exchange, &asset_pair)); + + if current_price_wrap.is_none() { + continue; + }; + // Example: sell orders + // + // In the list we had tasks such as + // - task1: sell when price > 10 + // - task2: sell when price > 20 + // - task3: sell when price > 30 + // If price used to be 5, and now it's 15, task1 got run + // If price used to be 5, and now it's 25, task1 and task2 got run + // If price used to be 5, and now it's 35, all tasks are run + // + // Example: buy orders + // + // In the list we had tasks such as + // - task1: buy when price < 10 + // - task2: buy when price < 20 + // - task3: buy when price < 30 + // If price used to be 500, and now it's 25, task3 got run + // If price used to be 500, and now it's 15, task2 and task3 got run + // If price used to be 500, and now it's 5, all tasks are run + // + // TODO: handle atomic and transaction + if let Some(mut tasks) = Self::get_sorted_tasks_index(&key) { + let current_price = current_price_wrap.unwrap(); + + for (&price, task_ids) in + (tasks.clone()).range(range_by_trigger_func(&trigger_func, ¤t_price)) + { + // Remove because we map this into task queue + tasks.remove(&price); + let t = &mut (&mut (task_ids.clone())); + task_to_process.append(t); + } + + // all tasks are moved to process, delete the queue + if tasks.is_empty() { + SortedTasksIndex::::remove(&key); + } else { + SortedTasksIndex::::insert(&key, tasks); + } + } + } + + if !task_to_process.is_empty() { + if TaskQueue::::exists() { + let mut old_task = TaskQueue::::get(); + old_task.append(task_to_process); + TaskQueue::::put(old_task); + } else { + TaskQueue::::put(task_to_process); + }; + } + + weight_left + } + + /// Trigger tasks for the block time. + /// + /// Complete as many tasks as possible given the maximum weight. + pub fn trigger_tasks(max_weight: Weight) -> Weight { + let mut weight_left: Weight = max_weight; + let check_time_and_deletion_weight = T::DbWeight::get().reads(2u64); + if weight_left.ref_time() < check_time_and_deletion_weight.ref_time() { + return weight_left; + } + + Self::shift_tasks(weight_left); + + // Now we can run those tasks + // TODO: We need to calculate enough weight and balance the tasks so we won't be skew + // by a particular kind of task asset + // + // Now we run as much task as possible + // If weight is over, task will be picked up next time + // If the price is no longer matched, they will be put back into the TaskRegistry + let task_queue = Self::get_task_queue(); + + weight_left = weight_left + // for above read + .saturating_sub(T::DbWeight::get().reads(1u64)) + // For measuring the TaskQueue::::put(tasks_left); + .saturating_sub(T::DbWeight::get().writes(1u64)); + if !task_queue.is_empty() { + let (tasks_left, new_weight_left) = Self::run_tasks(task_queue, weight_left); + weight_left = new_weight_left; + TaskQueue::::put(tasks_left); + } + + weight_left + } + + pub fn create_new_asset( + chain: ChainName, + exchange: Exchange, + asset1: AssetName, + asset2: AssetName, + decimal: u8, + asset_owners: Vec>, + ) -> Result<(), DispatchError> { + let key = (&chain, &exchange, (&asset1, &asset2)); + + if AssetRegistry::::contains_key(&key) { + Err(Error::::AssetAlreadyInitialized)? + } + + let asset_info = RegistryInfo:: { + decimal, + round: 0, + last_update: 0, + oracle_providers: asset_owners, + }; + + AssetRegistry::::insert(key, asset_info); + + Self::deposit_event(Event::AssetCreated { + chain, + exchange, + asset1, + asset2, + decimal, + }); + Ok(()) + } + + pub fn get_current_time_slot() -> Result> { + let now = >::get().saturated_into::(); + if now == 0 { + Err(Error::::BlockTimeNotSet)? + } + let now = now.saturating_div(1000); + let diff_to_min = now % 60; + Ok(now.saturating_sub(diff_to_min)) + } + + pub fn run_xcmp_task( + destination: Location, + caller: T::AccountId, + fee: AssetPayment, + encoded_call: Vec, + encoded_call_weight: Weight, + overall_weight: Weight, + flow: InstructionSequence, + ) -> (Weight, Option) { + let fee_asset_location = Location::try_from(fee.asset_location); + if fee_asset_location.is_err() { + return ( + ::WeightInfo::run_xcmp_task(), + Some(Error::::BadVersion.into()), + ); + } + let fee_asset_location = fee_asset_location.unwrap(); + + match T::XcmpTransactor::transact_xcm( + destination, + fee_asset_location, + fee.amount, + caller, + encoded_call, + encoded_call_weight, + overall_weight, + flow, + ) { + Ok(()) => (::WeightInfo::run_xcmp_task(), None), + Err(e) => (::WeightInfo::run_xcmp_task(), Some(e)), + } + } + + // return epoch time of current block + pub fn get_current_block_time() -> Result { + let now = >::get() + .checked_into::() + .ok_or(ArithmeticError::Overflow)?; + + if now == 0 { + Err(Error::::BlockTimeNotSet)?; + } + + let now = now.checked_div(1000).ok_or(ArithmeticError::Overflow)?; + Ok(now) + } + + // Check whether a task can run or not based on its expiration and price. + // + // A task can be queued but got expired when it's about to run, in that case, we don't want + // it to be run. + // + // Or the price might move by the time task is invoked, we don't want it to get run either. + fn task_can_run(task: &Task) -> (Option, Weight) { + let mut consumed_weight: Weight = Weight::zero(); + + // If we cannot extract time from the block, then somthing horrible wrong, let not move + // forward + let current_block_time = Self::get_current_block_time(); + if current_block_time.is_err() { + return (None, consumed_weight); + } + + let now = current_block_time.unwrap(); + + if task.expired_at < now.into() { + consumed_weight = + consumed_weight.saturating_add(::WeightInfo::emit_event()); + + Self::deposit_event(Event::TaskExpired { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: TaskCondition::AlreadyExpired { + expired_at: task.expired_at, + now: now.into(), + }, + }); + + return (None, consumed_weight); + } + + // read storage once to get the price + consumed_weight = consumed_weight.saturating_add(T::DbWeight::get().reads(1u64)); + if let Some(this_task_asset_price) = + Self::get_asset_price_data((&task.chain, &task.exchange, &task.asset_pair)) + { + if task.is_price_condition_match(&this_task_asset_price) { + return ( + Some(TaskCondition::TargetPriceMatched { + chain: task.chain.clone(), + exchange: task.exchange.clone(), + asset_pair: task.asset_pair.clone(), + price: this_task_asset_price.value, + }), + consumed_weight, + ); + } else { + Self::deposit_event(Event::PriceAlreadyMoved { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: TaskCondition::PriceAlreadyMoved { + chain: task.chain.clone(), + exchange: task.exchange.clone(), + asset_pair: task.asset_pair.clone(), + price: this_task_asset_price.value, + + target_price: task.trigger_params[0], + }, + }); + + return (None, consumed_weight); + } + } + + // This happen because we cannot find the price, so the task cannot be run + (None, consumed_weight) + } + + /// Runs as many tasks as the weight allows from the provided vec of task_ids. + /// + /// Returns a vec with the tasks that were not run and the remaining weight. + pub fn run_tasks( + mut task_ids: TaskIdList, + mut weight_left: Weight, + ) -> (TaskIdList, Weight) { + let mut consumed_task_index: usize = 0; + + // If we cannot extract time from the block, then somthing horrible wrong, let not move + // forward + let current_block_time = Self::get_current_block_time(); + if current_block_time.is_err() { + return (task_ids, weight_left); + } + + let _now = current_block_time.unwrap(); + + for (owner_id, task_id) in task_ids.iter() { + consumed_task_index.saturating_inc(); + + let action_weight = match Self::get_task(owner_id, task_id) { + None => { + Self::deposit_event(Event::TaskNotFound { + owner_id: owner_id.clone(), + task_id: task_id.clone(), + }); + ::WeightInfo::emit_event() + } + Some(task) => { + let (task_condition, test_can_run_weight) = Self::task_can_run(&task); + + if task_condition.is_none() { + test_can_run_weight + } else { + Self::deposit_event(Event::TaskTriggered { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: task_condition.unwrap(), + }); + + let _total_task = + Self::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v); + let _total_task_per_account = Self::get_account_stat( + &task.owner_id, + StatType::TotalTasksPerAccount, + ) + .map_or(0, |v| v); + + let (task_action_weight, task_dispatch_error) = + match task.action.clone() { + Action::XCMP { + destination, + execution_fee, + schedule_as, + encoded_call, + encoded_call_weight, + overall_weight, + instruction_sequence, + .. + } => Self::run_xcmp_task( + destination, + schedule_as.unwrap_or(task.owner_id.clone()), + execution_fee, + encoded_call, + encoded_call_weight, + overall_weight, + instruction_sequence, + ), + }; + + Self::remove_task(&task, None); + + if let Some(err) = task_dispatch_error { + Self::deposit_event(Event::::TaskExecutionFailed { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + error: err, + }); + } else { + Self::deposit_event(Event::::TaskExecuted { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + }); + } + + Self::deposit_event(Event::::TaskCompleted { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + }); + + task_action_weight + .saturating_add(T::DbWeight::get().writes(1u64)) + .saturating_add(T::DbWeight::get().reads(1u64)) + } + } + }; + + weight_left = weight_left.saturating_sub(action_weight); + + let run_another_task_weight = ::WeightInfo::emit_event() + .saturating_add(T::DbWeight::get().writes(1u64)) + .saturating_add(T::DbWeight::get().reads(1u64)); + if weight_left.ref_time() < run_another_task_weight.ref_time() { + break; + } + } + + if consumed_task_index == task_ids.len() { + (vec![], weight_left) + } else { + (task_ids.split_off(consumed_task_index), weight_left) + } + } + + // Handle task removal. There are a few places task need to be remove: + // - Tasks storage + // - TaskQueue if the task is already queued + // - TaskStats: decrease task count + // - AccountStats: decrease task count + // - SortedTasksIndex: sorted task by price + // - SortedTasksByExpiration: sorted task by expired epch + pub fn remove_task(task: &Task, event: Option>) { + Tasks::::remove(task.owner_id.clone(), task.task_id.clone()); + + // Remove it from SortedTasksIndex + let key = ( + &task.chain, + &task.exchange, + &task.asset_pair, + &task.trigger_function, + ); + if let Some(mut sorted_tasks_by_price) = Self::get_sorted_tasks_index(key) { + if let Some(tasks) = sorted_tasks_by_price.get_mut(&task.trigger_params[0]) { + if let Some(pos) = tasks.iter().position(|x| { + let (_, task_id) = x; + *task_id == task.task_id + }) { + tasks.remove(pos); + } + + if tasks.is_empty() { + // if there is no more task on this slot, clear it up + sorted_tasks_by_price.remove(&task.trigger_params[0].clone()); + } + SortedTasksIndex::::insert(&key, sorted_tasks_by_price); + } + } + + // Remove it from the SortedTasksByExpiration + SortedTasksByExpiration::::mutate(|sorted_tasks_by_expiration| { + if let Some(expired_task_slot) = + sorted_tasks_by_expiration.get_mut(&task.expired_at) + { + expired_task_slot.remove(&task.task_id); + if expired_task_slot.is_empty() { + sorted_tasks_by_expiration.remove(&task.expired_at); + } + } + }); + + // Update metrics + let total_task = Self::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v); + let total_task_per_account = + Self::get_account_stat(&task.owner_id, StatType::TotalTasksPerAccount) + .map_or(0, |v| v); + + if total_task >= 1 { + TaskStats::::insert(StatType::TotalTasksOverall, total_task - 1); + } + + if total_task_per_account >= 1 { + AccountStats::::insert( + task.owner_id.clone(), + StatType::TotalTasksPerAccount, + total_task_per_account - 1, + ); + } + + if let Some(e) = event { + Self::deposit_event(e); + } + } + + // Sweep as mucht ask we can and return the remaining weight + pub fn sweep_expired_task(remaining_weight: Weight) -> Weight { + if remaining_weight.ref_time() <= T::DbWeight::get().reads(1u64).ref_time() { + // Weight too low, not enough to do anything useful + return remaining_weight; + } + + let current_block_time = Self::get_current_block_time(); + + if current_block_time.is_err() { + // Cannot get time, this probably is the first block + return remaining_weight; + } + + let now = current_block_time.unwrap() as u128; + + // At the end we will most likely need to write back the updated storage, so here we + // account for that write + let mut unused_weight = remaining_weight + .saturating_sub(T::DbWeight::get().reads(1u64)) + .saturating_sub(T::DbWeight::get().writes(1u64)); + let mut tasks_by_expiration = Self::get_sorted_tasks_by_expiration(); + + let mut expired_shards: Vec = vec![]; + // Use Included(now) because if this task has not run at the end of this block, then + // that mean at next block it for sure will expired + 'outer: for (expired_time, task_ids) in + tasks_by_expiration.range_mut((Included(&0_u128), Included(&now))) + { + for (task_id, owner_id) in task_ids.iter() { + if unused_weight.ref_time() + > T::DbWeight::get() + .reads(1u64) + .saturating_add(::WeightInfo::remove_task()) + .ref_time() + { + unused_weight = unused_weight + .saturating_sub(T::DbWeight::get().reads(1u64)) + .saturating_sub(::WeightInfo::remove_task()); + + // Now let remove the task from chain storage + if let Some(task) = Self::get_task(owner_id, task_id) { + Self::remove_task( + &task, + Some(Event::TaskSweep { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: TaskCondition::AlreadyExpired { + expired_at: task.expired_at, + now, + }, + }), + ); + } + } else { + // If there is not enough weight left, break all the way out, we had + // already save one weight for the write to update storage back + break 'outer; + } + } + expired_shards.push(*expired_time); + } + + unused_weight + } + + // Task is write into a sorted storage, re-present by BTreeMap so we can find and expired them + pub fn track_expired_task(task: &Task) -> Result> { + // first we got back the reference to the underlying storage + // perform relevant update to write task to the right shard by expired + // time, then the value is store back to storage + let mut tasks_by_expiration = Self::get_sorted_tasks_by_expiration(); + + if let Some(task_shard) = tasks_by_expiration.get_mut(&task.expired_at) { + task_shard.insert(task.task_id.clone(), task.owner_id.clone()); + } else { + tasks_by_expiration.insert( + task.expired_at, + BTreeMap::from([(task.task_id.clone(), task.owner_id.clone())]), + ); + } + SortedTasksByExpiration::::put(tasks_by_expiration); + + Ok(true) + } + + /// With transaction will protect against a partial success where N of M execution times might be full, + /// rolling back any successful insertions into the schedule task table. + /// Validate and schedule task. + /// This will also charge the execution fee. + /// TODO: double check atomic + pub fn validate_and_schedule_task(task: Task) -> Result<(), Error> { + if task.task_id.is_empty() { + Err(Error::::InvalidTaskId)? + } + + let current_block_time = Self::get_current_block_time(); + if current_block_time.is_err() { + // Cannot get time, this probably is the first block + Err(Error::::BlockTimeNotSet)? + } + + let now = current_block_time.unwrap() as u128; + + if task.expired_at <= now { + Err(Error::::InvalidTaskExpiredAt)? + } + + let total_task = Self::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v); + let total_task_per_account = + Self::get_account_stat(&task.owner_id, StatType::TotalTasksPerAccount) + .map_or(0, |v| v); + + // check task total limit per account and overall + if total_task >= T::MaxTasksOverall::get().into() { + Err(Error::::MaxTasksReached)? + } + + // check task total limit per account and overall + if total_task_per_account >= T::MaxTasksPerAccount::get().into() { + Err(Error::::MaxTasksPerAccountReached)? + } + + match task.action.clone() { + Action::XCMP { + execution_fee, + instruction_sequence, + .. + } => { + let asset_location = Location::try_from(execution_fee.asset_location) + .map_err(|()| Error::::BadVersion)?; + let asset_location = asset_location + .reanchored( + &Location::new(1, Parachain(T::SelfParaId::get().into())), + &T::UniversalLocation::get(), + ) + .map_err(|_| Error::::CannotReanchor)?; + // Only native token are supported as the XCMP fee for local deductions + if instruction_sequence == InstructionSequence::PayThroughSovereignAccount + && asset_location != Location::new(0, Here) + { + Err(Error::::UnsupportedFeePayment)? + } + } + }; + + let fee_result = T::FeeHandler::pay_checked_fees_for( + &(task.owner_id.clone()), + &(task.action.clone()), + || { + Tasks::::insert(task.owner_id.clone(), task.task_id.clone(), &task); + + // Post task processing, increase relevant metrics data + TaskStats::::insert(StatType::TotalTasksOverall, total_task + 1); + AccountStats::::insert( + task.owner_id.clone(), + StatType::TotalTasksPerAccount, + total_task_per_account + 1, + ); + + let key = ( + &task.chain, + &task.exchange, + &task.asset_pair, + &task.trigger_function, + ); + + if let Some(mut sorted_task_index) = Self::get_sorted_tasks_index(key) { + // TODO: remove hard code and take right param + if let Some(tasks_by_price) = + sorted_task_index.get_mut(&(task.trigger_params[0])) + { + tasks_by_price.push((task.owner_id.clone(), task.task_id.clone())); + } else { + sorted_task_index.insert( + task.trigger_params[0], + vec![(task.owner_id.clone(), task.task_id.clone())], + ); + } + SortedTasksIndex::::insert(key, sorted_task_index); + } else { + let mut sorted_task_index = BTreeMap::>::new(); + sorted_task_index.insert( + task.trigger_params[0], + vec![(task.owner_id.clone(), task.task_id.clone())], + ); + + // TODO: sorted based on trigger_function comparison of the parameter + // then at the time of trigger we cut off all the left part of the tree + SortedTasksIndex::::insert(key, sorted_task_index); + }; + + Ok(()) + }, + ); + + if fee_result.is_err() { + Err(Error::::FeePaymentError)? + } + + if Self::track_expired_task(&task).is_err() { + Err(Error::::TaskExpiredStorageFailedToUpdate)? + } + + let schedule_as = match task.action.clone() { + Action::XCMP { schedule_as, .. } => schedule_as, + }; + + Self::deposit_event(Event::TaskScheduled { + owner_id: task.owner_id, + task_id: task.task_id, + schedule_as, + }); + Ok(()) + } + + /// Calculates the execution fee for a given action based on weight and num of executions + /// + /// Fee saturates at Weight/BalanceOf when there are an unreasonable num of executions + /// In practice, executions is bounded by T::MaxExecutionTimes and unlikely to saturate + pub fn calculate_schedule_fee_amount( + action: &ActionOf, + ) -> Result, DispatchError> { + let total_weight = action.execution_weight::()?; + + let schedule_fee_location = action.schedule_fee_location::(); + let schedule_fee_location = schedule_fee_location + .reanchored( + &Location::new(1, Parachain(T::SelfParaId::get().into())), + &T::UniversalLocation::get(), + ) + .map_err(|_| Error::::CannotReanchor)?; + + let fee = if schedule_fee_location == Location::default() { + T::ExecutionWeightFee::get() + .saturating_mul(>::saturated_from(total_weight)) + } else { + let raw_fee = + T::FeeConversionRateProvider::get_fee_per_second(&schedule_fee_location) + .ok_or("CouldNotDetermineFeePerSecond")? + .checked_mul(total_weight as u128) + .ok_or("FeeOverflow") + .map(|raw_fee| raw_fee / (WEIGHT_REF_TIME_PER_SECOND as u128))?; + >::saturated_from(raw_fee) + }; + + Ok(fee) + } + } } diff --git a/pallets/automation-price/src/mock.rs b/pallets/automation-price/src/mock.rs index 7a7ffd33e..9ad954b39 100644 --- a/pallets/automation-price/src/mock.rs +++ b/pallets/automation-price/src/mock.rs @@ -19,19 +19,19 @@ use super::*; use crate as pallet_automation_price; use crate::TaskId; +use ava_protocol_primitives::EnsureProxy; use frame_support::{ - assert_ok, construct_runtime, parameter_types, - traits::{ConstU32, Everything}, - weights::Weight, - PalletId, + assert_ok, construct_runtime, parameter_types, + traits::{ConstU32, Everything}, + weights::Weight, + PalletId, }; use frame_system::{self as system, RawOrigin}; use orml_traits::parameter_type_with_key; -use ava_protocol_primitives::EnsureProxy; use sp_core::H256; use sp_runtime::{ - traits::{AccountIdConversion, BlakeTwo256, Convert, IdentityLookup}, - AccountId32, Perbill, BuildStorage, + traits::{AccountIdConversion, BlakeTwo256, Convert, IdentityLookup}, + AccountId32, BuildStorage, Perbill, }; use sp_std::{marker::PhantomData, vec::Vec}; use staging_xcm::latest::{prelude::*, Junctions::*}; @@ -51,12 +51,18 @@ pub const PROXY_ACCOUNT: [u8; 32] = [4u8; 32]; pub const PARA_ID: u32 = 2000; pub const NATIVE: CurrencyId = 0; -pub const NATIVE_LOCATION: Location = Location { parents: 0, interior: Here }; +pub const NATIVE_LOCATION: Location = Location { + parents: 0, + interior: Here, +}; pub const NATIVE_EXECUTION_WEIGHT_FEE: u128 = 12; pub const FOREIGN_CURRENCY_ID: CurrencyId = 1; pub fn get_moonbase_asset_location() -> Location { - Location { parents: 1, interior: X2([Parachain(1000u32), PalletInstance(3u8)].into()) } + Location { + parents: 1, + interior: X2([Parachain(1000u32), PalletInstance(3u8)].into()), + } } pub const EXCHANGE1: &[u8] = "EXCHANGE1".as_bytes(); @@ -70,21 +76,21 @@ pub const ASSET3: &[u8] = "KSM".as_bytes(); pub const MOCK_XCMP_FEE: u128 = 10_000_000_u128; construct_runtime!( - pub enum Test - { - System: system, - Timestamp: pallet_timestamp, - Balances: pallet_balances, - ParachainInfo: parachain_info, - Tokens: orml_tokens, - Currencies: orml_currencies, - AutomationPrice: pallet_automation_price, - } + pub enum Test + { + System: system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + ParachainInfo: parachain_info, + Tokens: orml_tokens, + Currencies: orml_currencies, + AutomationPrice: pallet_automation_price, + } ); parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const SS58Prefix: u8 = 51; + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 51; } // impl system::Config for Test { @@ -116,429 +122,442 @@ parameter_types! { // } impl system::Config for Test { - type BaseCallFilter = Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Block = Block; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = AccountId32; - type Lookup = IdentityLookup; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; - type RuntimeTask = (); - type SingleBlockMigrations = (); - type MultiBlockMigrator = (); - type PreInherents = (); - type PostInherents = (); - type PostTransactions = (); + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId32; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type RuntimeTask = (); + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); } parameter_types! { - pub const ExistentialDeposit: u64 = 1; - pub const MaxLocks: u32 = 50; - pub const MaxReserves: u32 = 50; + pub const ExistentialDeposit: u64 = 1; + pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; } impl pallet_balances::Config for Test { - type MaxLocks = MaxLocks; - type Balance = Balance; - type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type MaxReserves = MaxReserves; - type ReserveIdentifier = [u8; 8]; - type FreezeIdentifier = (); - type MaxFreezes = ConstU32<0>; - type RuntimeHoldReason = (); - type RuntimeFreezeReason = (); - type WeightInfo = (); + type MaxLocks = MaxLocks; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<0>; + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); + type WeightInfo = (); } impl parachain_info::Config for Test {} parameter_type_with_key! { - pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { - Default::default() - }; + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + Default::default() + }; } parameter_types! { - pub DustAccount: AccountId = PalletId(*b"auto/dst").into_account_truncating(); + pub DustAccount: AccountId = PalletId(*b"auto/dst").into_account_truncating(); } impl orml_tokens::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Balance = Balance; - type Amount = i64; - type CurrencyId = CurrencyId; - type WeightInfo = (); - type ExistentialDeposits = ExistentialDeposits; - type CurrencyHooks = (); - type MaxLocks = ConstU32<100_000>; - type MaxReserves = ConstU32<100_000>; - type ReserveIdentifier = [u8; 8]; - type DustRemovalWhitelist = frame_support::traits::Nothing; + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = i64; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = ConstU32<100_000>; + type MaxReserves = ConstU32<100_000>; + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = frame_support::traits::Nothing; } impl orml_currencies::Config for Test { - type MultiCurrency = Tokens; - type NativeCurrency = AdaptedBasicCurrency; - type GetNativeCurrencyId = GetNativeCurrencyId; - type WeightInfo = (); + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; + type WeightInfo = (); } pub type AdaptedBasicCurrency = orml_currencies::BasicCurrencyAdapter; parameter_types! { - pub const MinimumPeriod: u64 = 1000; + pub const MinimumPeriod: u64 = 1000; } impl pallet_timestamp::Config for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = MinimumPeriod; - type WeightInfo = (); + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); } impl pallet_automation_price::Config for Test { - type RuntimeEvent = RuntimeEvent; - type MaxTasksPerSlot = MaxTasksPerSlot; - type MaxTasksPerAccount = MaxTasksPerAccount; - type MaxTasksOverall = MaxTasksOverall; - type MaxBlockWeight = MaxBlockWeight; - type MaxWeightPercentage = MaxWeightPercentage; - type WeightInfo = MockWeight; - type ExecutionWeightFee = ExecutionWeightFee; - type CurrencyId = CurrencyId; - type MultiCurrency = Currencies; - type Currency = Balances; - type CurrencyIdConvert = MockTokenIdConvert; - type FeeHandler = FeeHandler; - type FeeConversionRateProvider = MockConversionRateProvider; - type UniversalLocation = UniversalLocation; - type SelfParaId = parachain_info::Pallet; - type XcmpTransactor = MockXcmpTransactor; - - type EnsureProxy = MockEnsureProxy; + type RuntimeEvent = RuntimeEvent; + type MaxTasksPerSlot = MaxTasksPerSlot; + type MaxTasksPerAccount = MaxTasksPerAccount; + type MaxTasksOverall = MaxTasksOverall; + type MaxBlockWeight = MaxBlockWeight; + type MaxWeightPercentage = MaxWeightPercentage; + type WeightInfo = MockWeight; + type ExecutionWeightFee = ExecutionWeightFee; + type CurrencyId = CurrencyId; + type MultiCurrency = Currencies; + type Currency = Balances; + type CurrencyIdConvert = MockTokenIdConvert; + type FeeHandler = FeeHandler; + type FeeConversionRateProvider = MockConversionRateProvider; + type UniversalLocation = UniversalLocation; + type SelfParaId = parachain_info::Pallet; + type XcmpTransactor = MockXcmpTransactor; + + type EnsureProxy = MockEnsureProxy; } parameter_types! { - pub const MaxTasksPerSlot: u32 = 2; - // Mock value, purposely set to a small number so easiser to test limit reached - pub const MaxTasksOverall: u32 = 1024; - pub const MaxTasksPerAccount: u32 = 16; - #[derive(Debug)] - pub const MaxScheduleSeconds: u64 = 24 * 60 * 60; - pub const MaxBlockWeight: u64 = 20_000_000; - pub const MaxWeightPercentage: Perbill = Perbill::from_percent(40); - pub const ExecutionWeightFee: Balance = NATIVE_EXECUTION_WEIGHT_FEE; - - // When unit testing dynamic dispatch, we use the real weight value of the extrinsics call - // This is an external lib that we don't own so we try to not mock, follow the rule don't mock - // what you don't own - // One of test we do is Balances::transfer call, which has its weight define here: - // https://github.com/paritytech/polkadot-sdk/blob/polkadot-v0.9.38/frame/balances/src/weights.rs#L61-L73 - // When logging the final calculated amount, its value is 73_314_000. - // - // in our unit test, we test a few transfers with dynamic dispatch. On top - // of that, there is also weight of our call such as fetching the tasks, - // move from schedule slot to tasks queue,.. so the weight of a schedule - // transfer with dynamic dispatch is even higher. - // - // and because we test run a few of them so I set it to ~10x value of 73_314_000 - pub const MaxWeightPerSlot: u128 = 700_000_000; - pub const XmpFee: u128 = 1_000_000; - pub const GetNativeCurrencyId: CurrencyId = NATIVE; + pub const MaxTasksPerSlot: u32 = 2; + // Mock value, purposely set to a small number so easiser to test limit reached + pub const MaxTasksOverall: u32 = 1024; + pub const MaxTasksPerAccount: u32 = 16; + #[derive(Debug)] + pub const MaxScheduleSeconds: u64 = 24 * 60 * 60; + pub const MaxBlockWeight: u64 = 20_000_000; + pub const MaxWeightPercentage: Perbill = Perbill::from_percent(40); + pub const ExecutionWeightFee: Balance = NATIVE_EXECUTION_WEIGHT_FEE; + + // When unit testing dynamic dispatch, we use the real weight value of the extrinsics call + // This is an external lib that we don't own so we try to not mock, follow the rule don't mock + // what you don't own + // One of test we do is Balances::transfer call, which has its weight define here: + // https://github.com/paritytech/polkadot-sdk/blob/polkadot-v0.9.38/frame/balances/src/weights.rs#L61-L73 + // When logging the final calculated amount, its value is 73_314_000. + // + // in our unit test, we test a few transfers with dynamic dispatch. On top + // of that, there is also weight of our call such as fetching the tasks, + // move from schedule slot to tasks queue,.. so the weight of a schedule + // transfer with dynamic dispatch is even higher. + // + // and because we test run a few of them so I set it to ~10x value of 73_314_000 + pub const MaxWeightPerSlot: u128 = 700_000_000; + pub const XmpFee: u128 = 1_000_000; + pub const GetNativeCurrencyId: CurrencyId = NATIVE; } pub struct MockWeight(PhantomData); impl pallet_automation_price::WeightInfo for MockWeight { - fn emit_event() -> Weight { - Weight::from_parts(20_000_000_u64, 0u64) - } + fn emit_event() -> Weight { + Weight::from_parts(20_000_000_u64, 0u64) + } - fn asset_price_update_extrinsic(v: u32) -> Weight { - Weight::from_parts(220_000_000_u64 * v as u64, 0u64) - } + fn asset_price_update_extrinsic(v: u32) -> Weight { + Weight::from_parts(220_000_000_u64 * v as u64, 0u64) + } - fn initialize_asset_extrinsic(_v: u32) -> Weight { - Weight::from_parts(220_000_000_u64, 0u64) - } + fn initialize_asset_extrinsic(_v: u32) -> Weight { + Weight::from_parts(220_000_000_u64, 0u64) + } - fn schedule_xcmp_task_extrinsic() -> Weight { - Weight::from_parts(24_000_000_u64, 0u64) - } + fn schedule_xcmp_task_extrinsic() -> Weight { + Weight::from_parts(24_000_000_u64, 0u64) + } - fn cancel_task_extrinsic() -> Weight { - Weight::from_parts(20_000_000_u64, 0u64) - } + fn cancel_task_extrinsic() -> Weight { + Weight::from_parts(20_000_000_u64, 0u64) + } - fn run_xcmp_task() -> Weight { - Weight::from_parts(200_000_000_u64, 0u64) - } + fn run_xcmp_task() -> Weight { + Weight::from_parts(200_000_000_u64, 0u64) + } - fn remove_task() -> Weight { - Weight::from_parts(20_000_000_u64, 0u64) - } + fn remove_task() -> Weight { + Weight::from_parts(20_000_000_u64, 0u64) + } } pub struct MockXcmpTransactor(PhantomData<(T, C)>); impl pallet_xcmp_handler::XcmpTransactor - for MockXcmpTransactor + for MockXcmpTransactor where - T: Config + pallet::Config, - C: frame_support::traits::ReservableCurrency, + T: Config + pallet::Config, + C: frame_support::traits::ReservableCurrency, { - fn transact_xcm( - _destination: Location, - _location: staging_xcm::latest::Location, - _fee: u128, - _caller: T::AccountId, - _transact_encoded_call: sp_std::vec::Vec, - _transact_encoded_call_weight: Weight, - _overall_weight: Weight, - _flow: InstructionSequence, - ) -> Result<(), sp_runtime::DispatchError> { - Ok(()) - } - - fn pay_xcm_fee( - _: CurrencyId, - _: T::AccountId, - _: u128, - ) -> Result<(), sp_runtime::DispatchError> { - Ok(()) - } + fn transact_xcm( + _destination: Location, + _location: staging_xcm::latest::Location, + _fee: u128, + _caller: T::AccountId, + _transact_encoded_call: sp_std::vec::Vec, + _transact_encoded_call_weight: Weight, + _overall_weight: Weight, + _flow: InstructionSequence, + ) -> Result<(), sp_runtime::DispatchError> { + Ok(()) + } + + fn pay_xcm_fee( + _: CurrencyId, + _: T::AccountId, + _: u128, + ) -> Result<(), sp_runtime::DispatchError> { + Ok(()) + } } pub struct MockConversionRateProvider; impl FixedConversionRateProvider for MockConversionRateProvider { - fn get_fee_per_second(location: &Location) -> Option { - get_fee_per_second(location) - } + fn get_fee_per_second(location: &Location) -> Option { + get_fee_per_second(location) + } } pub struct MockTokenIdConvert; impl Convert> for MockTokenIdConvert { - fn convert(id: CurrencyId) -> Option { - if id == NATIVE { - Some(Location::new(0, Here)) - } else if id == FOREIGN_CURRENCY_ID { - Some(Location::new(1, Parachain(PARA_ID))) - } else { - None - } - } + fn convert(id: CurrencyId) -> Option { + if id == NATIVE { + Some(Location::new(0, Here)) + } else if id == FOREIGN_CURRENCY_ID { + Some(Location::new(1, Parachain(PARA_ID))) + } else { + None + } + } } impl Convert> for MockTokenIdConvert { - fn convert(location: Location) -> Option { - if location == Location::new(0, Here) { - Some(NATIVE) - } else if location == Location::new(1, Parachain(PARA_ID)) { - Some(FOREIGN_CURRENCY_ID) - } else { - None - } - } + fn convert(location: Location) -> Option { + if location == Location::new(0, Here) { + Some(NATIVE) + } else if location == Location::new(1, Parachain(PARA_ID)) { + Some(FOREIGN_CURRENCY_ID) + } else { + None + } + } } // TODO: We should extract this and share code with automation-time pub struct MockEnsureProxy; impl EnsureProxy for MockEnsureProxy { - fn ensure_ok(_delegator: AccountId, _delegatee: AccountId) -> Result<(), &'static str> { - if _delegator == DELEGATOR_ACCOUNT.into() && _delegatee == PROXY_ACCOUNT.into() { - Ok(()) - } else { - Err("proxy error: expected `ProxyType::Any`") - } - } + fn ensure_ok(_delegator: AccountId, _delegatee: AccountId) -> Result<(), &'static str> { + if _delegator == DELEGATOR_ACCOUNT.into() && _delegatee == PROXY_ACCOUNT.into() { + Ok(()) + } else { + Err("proxy error: expected `ProxyType::Any`") + } + } } parameter_types! { - pub const RelayNetwork: NetworkId = NetworkId::Rococo; - // The universal location within the global consensus system - pub UniversalLocation: InteriorLocation = X2([GlobalConsensus(RelayNetwork::get()), Parachain(ParachainInfo::parachain_id().into())].into()); + pub const RelayNetwork: NetworkId = NetworkId::Rococo; + // The universal location within the global consensus system + pub UniversalLocation: InteriorLocation = X2([GlobalConsensus(RelayNetwork::get()), Parachain(ParachainInfo::parachain_id().into())].into()); } // Build genesis storage according to the mock runtime. pub fn new_test_ext(state_block_time: u64) -> sp_io::TestExternalities { - let genesis_storage = system::GenesisConfig::::default().build_storage().unwrap(); - let mut ext = sp_io::TestExternalities::new(genesis_storage); - ext.execute_with(|| System::set_block_number(1)); - ext.execute_with(|| Timestamp::set_timestamp(state_block_time)); - ext + let genesis_storage = system::GenesisConfig::::default() + .build_storage() + .unwrap(); + let mut ext = sp_io::TestExternalities::new(genesis_storage); + ext.execute_with(|| System::set_block_number(1)); + ext.execute_with(|| Timestamp::set_timestamp(state_block_time)); + ext } pub fn events() -> Vec { - let events = System::events(); - let evt = events.into_iter().map(|evt| evt.event).collect::>(); + let events = System::events(); + let evt = events.into_iter().map(|evt| evt.event).collect::>(); - System::reset_events(); + System::reset_events(); - evt + evt } // A utility test function to pluck out the task id from events, useful when dealing with multiple // task scheduling pub fn get_task_ids_from_events() -> Vec { - System::events() - .into_iter() - .filter_map(|e| match e.event { - RuntimeEvent::AutomationPrice(crate::Event::TaskScheduled { task_id, .. }) => { - Some(task_id) - }, - _ => None, - }) - .collect::>() + System::events() + .into_iter() + .filter_map(|e| match e.event { + RuntimeEvent::AutomationPrice(crate::Event::TaskScheduled { task_id, .. }) => { + Some(task_id) + } + _ => None, + }) + .collect::>() } pub fn get_xcmp_funds(account: AccountId) { - let double_action_weight = MockWeight::::run_xcmp_task() * 2; - let action_fee = ExecutionWeightFee::get() * u128::from(double_action_weight.ref_time()); - let max_execution_fee = action_fee * u128::from(1u32); - let with_xcm_fees = max_execution_fee + XmpFee::get(); - Balances::force_set_balance(RawOrigin::Root.into(), account, with_xcm_fees).unwrap(); + let double_action_weight = MockWeight::::run_xcmp_task() * 2; + let action_fee = ExecutionWeightFee::get() * u128::from(double_action_weight.ref_time()); + let max_execution_fee = action_fee * u128::from(1u32); + let with_xcm_fees = max_execution_fee + XmpFee::get(); + Balances::force_set_balance(RawOrigin::Root.into(), account, with_xcm_fees).unwrap(); } #[derive(Clone)] pub struct MockAssetFeePerSecond { - pub asset_location: Location, - pub fee_per_second: u128, + pub asset_location: Location, + pub fee_per_second: u128, } pub fn get_asset_fee_per_second_config() -> Vec { - let asset_fee_per_second: [MockAssetFeePerSecond; 3] = [ - MockAssetFeePerSecond { - asset_location: Location { parents: 1, interior: Parachain(2000).into() }, - fee_per_second: 416_000_000_000, - }, - MockAssetFeePerSecond { - asset_location: Location { - parents: 1, - interior: X2([Parachain(2110), GeneralKey { length: 4, data: [0; 32] }].into()), - }, - fee_per_second: 416_000_000_000, - }, - MockAssetFeePerSecond { - asset_location: get_moonbase_asset_location(), - fee_per_second: 10_000_000_000_000_000_000, - }, - ]; - asset_fee_per_second.to_vec() + let asset_fee_per_second: [MockAssetFeePerSecond; 3] = [ + MockAssetFeePerSecond { + asset_location: Location { + parents: 1, + interior: Parachain(2000).into(), + }, + fee_per_second: 416_000_000_000, + }, + MockAssetFeePerSecond { + asset_location: Location { + parents: 1, + interior: X2([ + Parachain(2110), + GeneralKey { + length: 4, + data: [0; 32], + }, + ] + .into()), + }, + fee_per_second: 416_000_000_000, + }, + MockAssetFeePerSecond { + asset_location: get_moonbase_asset_location(), + fee_per_second: 10_000_000_000_000_000_000, + }, + ]; + asset_fee_per_second.to_vec() } pub fn get_fee_per_second(location: &Location) -> Option { - let location = location.clone() - .reanchored( - &Location::new(1, Parachain(::SelfParaId::get().into())), - &::UniversalLocation::get(), - ) - .expect("Reanchor location failed"); - - let found_asset = get_asset_fee_per_second_config().into_iter().find(|item| { - let MockAssetFeePerSecond { asset_location, .. } = item; - *asset_location == location - }); - - if let Some(asset) = found_asset { - Some(asset.fee_per_second) - } else { - None - } + let location = location + .clone() + .reanchored( + &Location::new(1, Parachain(::SelfParaId::get().into())), + &::UniversalLocation::get(), + ) + .expect("Reanchor location failed"); + + let found_asset = get_asset_fee_per_second_config().into_iter().find(|item| { + let MockAssetFeePerSecond { asset_location, .. } = item; + *asset_location == location + }); + + if let Some(asset) = found_asset { + Some(asset.fee_per_second) + } else { + None + } } // setup a sample default asset to support test pub fn setup_asset(sender: &AccountId32, chain: Vec) { - let _ = AutomationPrice::initialize_asset( - RawOrigin::Root.into(), - chain, - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - 10, - vec![sender.clone()], - ); + let _ = AutomationPrice::initialize_asset( + RawOrigin::Root.into(), + chain, + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + 10, + vec![sender.clone()], + ); } // setup a few sample assets, initialize it with sane default vale and set a price to support test cases pub fn setup_assets_and_prices(sender: &AccountId32, block_time: u128) { - let _ = AutomationPrice::initialize_asset( - RawOrigin::Root.into(), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - 10, - vec![sender.clone()], - ); - - let _ = AutomationPrice::initialize_asset( - RawOrigin::Root.into(), - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - ASSET2.to_vec(), - ASSET3.to_vec(), - 10, - vec![sender.clone()], - ); - - let _ = AutomationPrice::initialize_asset( - RawOrigin::Root.into(), - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET3.to_vec(), - 10, - vec![sender.clone()], - ); - - // This fixture function initialize 3 asset pairs, and set their price to 1000, 5000, 10_000 - const PAIR1_PRICE: u128 = 1000_u128; - const PAIR2_PRICE: u128 = 5000_u128; - const PAIR3_PRICE: u128 = 10_000_u128; - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender.clone()), - vec![CHAIN1.to_vec()], - vec![EXCHANGE1.to_vec()], - vec![ASSET1.to_vec()], - vec![ASSET2.to_vec()], - vec![PAIR1_PRICE], - vec![block_time], - vec![1000], - )); - - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender.clone()), - vec![CHAIN2.to_vec()], - vec![EXCHANGE1.to_vec()], - vec![ASSET2.to_vec()], - vec![ASSET3.to_vec()], - vec![PAIR2_PRICE], - vec![block_time], - vec![1000], - )); - - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender.clone()), - vec![CHAIN2.to_vec()], - vec![EXCHANGE1.to_vec()], - vec![ASSET1.to_vec()], - vec![ASSET3.to_vec()], - vec![PAIR3_PRICE], - vec![block_time], - vec![1000], - )); + let _ = AutomationPrice::initialize_asset( + RawOrigin::Root.into(), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + 10, + vec![sender.clone()], + ); + + let _ = AutomationPrice::initialize_asset( + RawOrigin::Root.into(), + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + ASSET2.to_vec(), + ASSET3.to_vec(), + 10, + vec![sender.clone()], + ); + + let _ = AutomationPrice::initialize_asset( + RawOrigin::Root.into(), + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET3.to_vec(), + 10, + vec![sender.clone()], + ); + + // This fixture function initialize 3 asset pairs, and set their price to 1000, 5000, 10_000 + const PAIR1_PRICE: u128 = 1000_u128; + const PAIR2_PRICE: u128 = 5000_u128; + const PAIR3_PRICE: u128 = 10_000_u128; + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender.clone()), + vec![CHAIN1.to_vec()], + vec![EXCHANGE1.to_vec()], + vec![ASSET1.to_vec()], + vec![ASSET2.to_vec()], + vec![PAIR1_PRICE], + vec![block_time], + vec![1000], + )); + + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender.clone()), + vec![CHAIN2.to_vec()], + vec![EXCHANGE1.to_vec()], + vec![ASSET2.to_vec()], + vec![ASSET3.to_vec()], + vec![PAIR2_PRICE], + vec![block_time], + vec![1000], + )); + + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender.clone()), + vec![CHAIN2.to_vec()], + vec![EXCHANGE1.to_vec()], + vec![ASSET1.to_vec()], + vec![ASSET3.to_vec()], + vec![PAIR3_PRICE], + vec![block_time], + vec![1000], + )); } diff --git a/pallets/automation-price/src/tests.rs b/pallets/automation-price/src/tests.rs index 63b32da35..eac8a66ed 100644 --- a/pallets/automation-price/src/tests.rs +++ b/pallets/automation-price/src/tests.rs @@ -16,15 +16,12 @@ // limitations under the License. use crate::{ - mock::*, AccountStats, Action, AssetPayment, Error, StatType, Task, TaskIdList, - TaskStats, Tasks, + mock::*, AccountStats, Action, AssetPayment, Error, StatType, Task, TaskIdList, TaskStats, + Tasks, }; use pallet_xcmp_handler::InstructionSequence; -use frame_support::{ - assert_noop, assert_ok, - weights::Weight, -}; +use frame_support::{assert_noop, assert_ok, weights::Weight}; use frame_system::{self, RawOrigin}; use sp_runtime::{AccountId32, ArithmeticError}; @@ -39,418 +36,433 @@ pub const START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND: u128 = 33198768000 + 3600; /// Assert the given `event` exists. #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))] pub fn assert_has_event(event: RuntimeEvent) { - let evts = System::events().into_iter().map(|evt| evt.event).collect::>(); - assert!(evts.iter().any(|record| record == &event)) + let evts = System::events() + .into_iter() + .map(|evt| evt.event) + .collect::>(); + assert!(evts.iter().any(|record| record == &event)) } // Helper function to asset event easiser /// Assert the given `event` not exists. #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))] pub fn assert_no_event(event: RuntimeEvent) { - let evts = System::events().into_iter().map(|evt| evt.event).collect::>(); - assert!(evts.iter().all(|record| record != &event)) + let evts = System::events() + .into_iter() + .map(|evt| evt.event) + .collect::>(); + assert!(evts.iter().all(|record| record != &event)) } #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))] pub fn assert_last_event(event: RuntimeEvent) { - assert_eq!(events().last().expect("events expected"), &event); + assert_eq!(events().last().expect("events expected"), &event); } #[test] fn test_initialize_asset_works() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let sender = AccountId32::new(ALICE); - assert_ok!(AutomationPrice::initialize_asset( - RawOrigin::Root.into(), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - 10, - vec!(sender) - )); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetCreated { - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset1: ASSET1.to_vec(), - asset2: ASSET2.to_vec(), - decimal: 10, - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let sender = AccountId32::new(ALICE); + assert_ok!(AutomationPrice::initialize_asset( + RawOrigin::Root.into(), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + 10, + vec!(sender) + )); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetCreated { + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset1: ASSET1.to_vec(), + asset2: ASSET2.to_vec(), + decimal: 10, + })); + }) } #[test] fn test_initialize_asset_reject_duplicate_asset() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let sender = AccountId32::new(ALICE); - let _ = AutomationPrice::initialize_asset( - RawOrigin::Root.into(), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - 10, - vec![sender.clone()], - ); - - assert_noop!( - AutomationPrice::initialize_asset( - RawOrigin::Root.into(), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - 10, - vec!(sender) - ), - Error::::AssetAlreadyInitialized, - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let sender = AccountId32::new(ALICE); + let _ = AutomationPrice::initialize_asset( + RawOrigin::Root.into(), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + 10, + vec![sender.clone()], + ); + + assert_noop!( + AutomationPrice::initialize_asset( + RawOrigin::Root.into(), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + 10, + vec!(sender) + ), + Error::::AssetAlreadyInitialized, + ); + }) } #[test] fn test_update_asset_prices() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let sender = AccountId32::new(ALICE); - - setup_asset(&sender, CHAIN1.to_vec()); - - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender.clone()), - vec!(CHAIN1.to_vec()), - vec!(EXCHANGE1.to_vec()), - vec!(ASSET1.to_vec()), - vec!(ASSET2.to_vec()), - vec!(1005), - vec!(START_BLOCK_TIME as u128), - vec!(1), - )); - - let p = AutomationPrice::get_asset_price_data(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - )) - .expect("cannot get price"); - - assert_eq!(p.round, 1); - assert_eq!(p.value, 1005); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetUpdated { - owner_id: sender, - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset1: ASSET1.to_vec(), - asset2: ASSET2.to_vec(), - price: 1005, - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let sender = AccountId32::new(ALICE); + + setup_asset(&sender, CHAIN1.to_vec()); + + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender.clone()), + vec!(CHAIN1.to_vec()), + vec!(EXCHANGE1.to_vec()), + vec!(ASSET1.to_vec()), + vec!(ASSET2.to_vec()), + vec!(1005), + vec!(START_BLOCK_TIME as u128), + vec!(1), + )); + + let p = AutomationPrice::get_asset_price_data(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + )) + .expect("cannot get price"); + + assert_eq!(p.round, 1); + assert_eq!(p.value, 1005); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetUpdated { + owner_id: sender, + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset1: ASSET1.to_vec(), + asset2: ASSET2.to_vec(), + price: 1005, + })); + }) } #[test] fn test_update_asset_price_increase_round() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let sender = AccountId32::new(ALICE); - - setup_asset(&sender, CHAIN1.to_vec()); - - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender.clone()), - vec!(CHAIN1.to_vec()), - vec!(EXCHANGE1.to_vec()), - vec!(ASSET1.to_vec()), - vec!(ASSET2.to_vec()), - vec!(1005), - vec!(START_BLOCK_TIME as u128), - vec!(1), - )); - - let p = AutomationPrice::get_asset_price_data(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - )) - .expect("cannot get price"); - - assert_eq!(p.round, 1); - assert_eq!(p.updated_at, (START_BLOCK_TIME / 1000).into()); - - Timestamp::set_timestamp( - (START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND * 1000).try_into().unwrap(), - ); - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender), - vec!(CHAIN1.to_vec()), - vec!(EXCHANGE1.to_vec()), - vec!(ASSET1.to_vec()), - vec!(ASSET2.to_vec()), - vec!(1005), - vec!(START_BLOCK_TIME as u128), - vec!(1), - )); - - let p = AutomationPrice::get_asset_price_data(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - )) - .expect("cannot get price"); - - assert_eq!(p.round, 2); - assert_eq!(p.updated_at, START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let sender = AccountId32::new(ALICE); + + setup_asset(&sender, CHAIN1.to_vec()); + + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender.clone()), + vec!(CHAIN1.to_vec()), + vec!(EXCHANGE1.to_vec()), + vec!(ASSET1.to_vec()), + vec!(ASSET2.to_vec()), + vec!(1005), + vec!(START_BLOCK_TIME as u128), + vec!(1), + )); + + let p = AutomationPrice::get_asset_price_data(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + )) + .expect("cannot get price"); + + assert_eq!(p.round, 1); + assert_eq!(p.updated_at, (START_BLOCK_TIME / 1000).into()); + + Timestamp::set_timestamp( + (START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND * 1000) + .try_into() + .unwrap(), + ); + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender), + vec!(CHAIN1.to_vec()), + vec!(EXCHANGE1.to_vec()), + vec!(ASSET1.to_vec()), + vec!(ASSET2.to_vec()), + vec!(1005), + vec!(START_BLOCK_TIME as u128), + vec!(1), + )); + + let p = AutomationPrice::get_asset_price_data(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + )) + .expect("cannot get price"); + + assert_eq!(p.round, 2); + assert_eq!(p.updated_at, START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND); + }) } #[test] fn test_update_asset_prices_multi() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let sender = AccountId32::new(ALICE); - - setup_asset(&sender, CHAIN1.to_vec()); - setup_asset(&sender, CHAIN2.to_vec()); - - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender.clone()), - vec!(CHAIN1.to_vec(), CHAIN2.to_vec()), - vec!(EXCHANGE1.to_vec(), EXCHANGE1.to_vec()), - vec!(ASSET1.to_vec(), ASSET1.to_vec()), - vec!(ASSET2.to_vec(), ASSET2.to_vec()), - vec!(1005, 1009), - vec!(START_BLOCK_TIME as u128, START_BLOCK_TIME as u128), - vec!(1, 2), - )); - - let p1 = AutomationPrice::get_asset_price_data(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - )) - .expect("cannot get price"); - - assert_eq!(p1.round, 1); - assert_eq!(p1.value, 1005); - - let p2 = AutomationPrice::get_asset_price_data(( - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - )) - .expect("cannot get price"); - - assert_eq!(p2.round, 2); - assert_eq!(p2.value, 1009); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetUpdated { - owner_id: sender.clone(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset1: ASSET1.to_vec(), - asset2: ASSET2.to_vec(), - price: 1005, - })); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetUpdated { - owner_id: sender, - chain: CHAIN2.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset1: ASSET1.to_vec(), - asset2: ASSET2.to_vec(), - price: 1009, - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let sender = AccountId32::new(ALICE); + + setup_asset(&sender, CHAIN1.to_vec()); + setup_asset(&sender, CHAIN2.to_vec()); + + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender.clone()), + vec!(CHAIN1.to_vec(), CHAIN2.to_vec()), + vec!(EXCHANGE1.to_vec(), EXCHANGE1.to_vec()), + vec!(ASSET1.to_vec(), ASSET1.to_vec()), + vec!(ASSET2.to_vec(), ASSET2.to_vec()), + vec!(1005, 1009), + vec!(START_BLOCK_TIME as u128, START_BLOCK_TIME as u128), + vec!(1, 2), + )); + + let p1 = AutomationPrice::get_asset_price_data(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + )) + .expect("cannot get price"); + + assert_eq!(p1.round, 1); + assert_eq!(p1.value, 1005); + + let p2 = AutomationPrice::get_asset_price_data(( + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + )) + .expect("cannot get price"); + + assert_eq!(p2.round, 2); + assert_eq!(p2.value, 1009); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetUpdated { + owner_id: sender.clone(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset1: ASSET1.to_vec(), + asset2: ASSET2.to_vec(), + price: 1005, + })); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetUpdated { + owner_id: sender, + chain: CHAIN2.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset1: ASSET1.to_vec(), + asset2: ASSET2.to_vec(), + price: 1009, + })); + }) } #[test] fn test_schedule_xcmp_task_ok() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let para_id: u32 = 1000; - let creator = AccountId32::new(ALICE); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_asset(&creator, CHAIN1.to_vec()); - - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator.clone()), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.clone().into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call.clone(), - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - - // Upon schedule, task will be insert into 3 places - // 1. TaskRegistry: a fast hashmap look up using task id only - // 2. SortedTasksIndex: an ordering BTreeMap of the task, only task id and its price - // trigger - // 3. AccountTasks: hashmap to look up user task id - - let task_ids = get_task_ids_from_events(); - let task_id = task_ids.first().expect("task failed to schedule"); - - let task = AutomationPrice::get_task(&creator, &task_id).expect("missing task in registry"); - assert_eq!( - task.trigger_function, - "gt".as_bytes().to_vec(), - "created task has wrong trigger function" - ); - assert_eq!(task.chain, CHAIN1.to_vec(), "created task has different chain id"); - assert_eq!(task.asset_pair.0, ASSET1, "created task has wrong asset pair"); - - assert_eq!(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, task.expired_at); - - // Ensure task is inserted into the right SortedIndex - - // Create second task, and make sure both are recorded - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator.clone()), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - let task_ids2 = get_task_ids_from_events(); - let task_id2 = task_ids2.last().expect("task failed to schedule"); - assert_ne!(task_id, task_id2, "task id dup"); - - let sorted_task_index = AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )) - .unwrap(); - let task_ids: Vec> = sorted_task_index.into_values().collect(); - assert_eq!( - task_ids, - vec!(vec!( - (creator.clone(), "1-0-4".as_bytes().to_vec()), - (creator.clone(), "1-0-7".as_bytes().to_vec()), - )) - ); - - // We had schedule 2 tasks so far, all two belong to the same account - assert_eq!( - 2, - AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), - "total task count is incorrect" - ); - assert_eq!( - 2, - AutomationPrice::get_account_stat(creator, StatType::TotalTasksPerAccount) - .map_or(0, |v| v), - "total task count is incorrect" - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let para_id: u32 = 1000; + let creator = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_asset(&creator, CHAIN1.to_vec()); + + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator.clone()), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.clone().into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call.clone(), + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + + // Upon schedule, task will be insert into 3 places + // 1. TaskRegistry: a fast hashmap look up using task id only + // 2. SortedTasksIndex: an ordering BTreeMap of the task, only task id and its price + // trigger + // 3. AccountTasks: hashmap to look up user task id + + let task_ids = get_task_ids_from_events(); + let task_id = task_ids.first().expect("task failed to schedule"); + + let task = AutomationPrice::get_task(&creator, &task_id).expect("missing task in registry"); + assert_eq!( + task.trigger_function, + "gt".as_bytes().to_vec(), + "created task has wrong trigger function" + ); + assert_eq!( + task.chain, + CHAIN1.to_vec(), + "created task has different chain id" + ); + assert_eq!( + task.asset_pair.0, ASSET1, + "created task has wrong asset pair" + ); + + assert_eq!(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, task.expired_at); + + // Ensure task is inserted into the right SortedIndex + + // Create second task, and make sure both are recorded + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator.clone()), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + let task_ids2 = get_task_ids_from_events(); + let task_id2 = task_ids2.last().expect("task failed to schedule"); + assert_ne!(task_id, task_id2, "task id dup"); + + let sorted_task_index = AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )) + .unwrap(); + let task_ids: Vec> = sorted_task_index.into_values().collect(); + assert_eq!( + task_ids, + vec!(vec!( + (creator.clone(), "1-0-4".as_bytes().to_vec()), + (creator.clone(), "1-0-7".as_bytes().to_vec()), + )) + ); + + // We had schedule 2 tasks so far, all two belong to the same account + assert_eq!( + 2, + AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), + "total task count is incorrect" + ); + assert_eq!( + 2, + AutomationPrice::get_account_stat(creator, StatType::TotalTasksPerAccount) + .map_or(0, |v| v), + "total task count is incorrect" + ); + }) } // Verify when user having not enough fund, we will fail with the right error code #[test] fn test_schedule_xcmp_task_fail_not_enough_balance() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let para_id: u32 = 1000; - let creator = AccountId32::new(ALICE); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_asset(&creator, CHAIN1.to_vec()); - - get_xcmp_funds(creator.clone()); - assert_noop!( - AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - // Make a really high fee to simulate not enough balance - amount: MOCK_XCMP_FEE * 10_000 - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - ), - Error::::FeePaymentError, - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let para_id: u32 = 1000; + let creator = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_asset(&creator, CHAIN1.to_vec()); + + get_xcmp_funds(creator.clone()); + assert_noop!( + AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + // Make a really high fee to simulate not enough balance + amount: MOCK_XCMP_FEE * 10_000 + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + ), + Error::::FeePaymentError, + ); + }) } // Verify that upon scheduling a task, the task expiration will be inserted into // SortedTasksByExpiration and shard by expired_at. #[test] fn test_schedule_put_task_to_expiration_queue() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let para_id: u32 = 1000; - let creator = AccountId32::new(ALICE); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); - // Lets setup 3 tasks - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator.clone()), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - let task_ids = get_task_ids_from_events(); - let task_id = task_ids.last().expect("task failed to schedule"); - - let task_expiration_map = AutomationPrice::get_sorted_tasks_by_expiration(); - assert_eq!( - task_expiration_map - .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND)) - .expect("missing task expiration shard"), - &(BTreeMap::from([(task_id.clone(), creator)])) - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let para_id: u32 = 1000; + let creator = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); + // Lets setup 3 tasks + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator.clone()), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + let task_ids = get_task_ids_from_events(); + let task_id = task_ids.last().expect("task failed to schedule"); + + let task_expiration_map = AutomationPrice::get_sorted_tasks_by_expiration(); + assert_eq!( + task_expiration_map + .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND)) + .expect("missing task expiration shard"), + &(BTreeMap::from([(task_id.clone(), creator)])) + ); + }) } // Verify that upon scheduling a task, the task expiration will be inserted into @@ -459,451 +471,450 @@ fn test_schedule_put_task_to_expiration_queue() { // ensure all of them got to the right spot by expired_at time. #[test] fn test_schedule_put_task_to_expiration_queue_multi() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let para_id: u32 = 1000; - let creator1 = AccountId32::new(ALICE); - let creator2 = AccountId32::new(BOB); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_asset(&creator1, CHAIN1.to_vec()); - - get_xcmp_funds(creator1.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator1.clone()), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.clone().into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: 100_000 - }), - call.clone(), - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - let task_ids1 = get_task_ids_from_events(); - let task_id1 = task_ids1.last().expect("task failed to schedule"); - - get_xcmp_funds(creator2.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator2.clone()), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600, - "lt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - let task_ids2 = get_task_ids_from_events(); - let task_id2 = task_ids2.last().expect("task failed to schedule"); - - let task_expiration_map = AutomationPrice::get_sorted_tasks_by_expiration(); - assert_eq!( - task_expiration_map - .get(&START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND) - .expect("missing task expiration shard"), - &BTreeMap::from([(task_id1.clone(), creator1)]), - ); - assert_eq!( - task_expiration_map - .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600)) - .expect("missing task expiration shard"), - &BTreeMap::from([(task_id2.clone(), creator2)]), - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let para_id: u32 = 1000; + let creator1 = AccountId32::new(ALICE); + let creator2 = AccountId32::new(BOB); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_asset(&creator1, CHAIN1.to_vec()); + + get_xcmp_funds(creator1.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator1.clone()), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.clone().into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: 100_000 + }), + call.clone(), + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + let task_ids1 = get_task_ids_from_events(); + let task_id1 = task_ids1.last().expect("task failed to schedule"); + + get_xcmp_funds(creator2.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator2.clone()), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600, + "lt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + let task_ids2 = get_task_ids_from_events(); + let task_id2 = task_ids2.last().expect("task failed to schedule"); + + let task_expiration_map = AutomationPrice::get_sorted_tasks_by_expiration(); + assert_eq!( + task_expiration_map + .get(&START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND) + .expect("missing task expiration shard"), + &BTreeMap::from([(task_id1.clone(), creator1)]), + ); + assert_eq!( + task_expiration_map + .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600)) + .expect("missing task expiration shard"), + &BTreeMap::from([(task_id2.clone(), creator2)]), + ); + }) } // Verify that after calling sweep, expired task will be removed from all relevant storage. Our // stat is also decrease accordingly to the task removal #[test] fn test_sweep_expired_task_works() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let creator = AccountId32::new(ALICE); - let other_creator = AccountId32::new(BOB); - let para_id: u32 = 1000; - - setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); - - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: MOCK_XCMP_FEE, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - - let expired_task_gen = 10; - let price_target1 = 2000; - for i in 0..expired_task_gen { - // schedule task that has expired - get_xcmp_funds(creator.clone()); - let task = Task:: { - owner_id: creator.clone(), - task_id: format!("123-0-{:?}", i).as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800, - trigger_function: "gt".as_bytes().to_vec(), - trigger_params: vec![price_target1], - action: Action::XCMP { - destination: destination.clone(), - schedule_fee: schedule_fee.clone(), - execution_fee: execution_fee.clone(), - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - assert_ok!(AutomationPrice::validate_and_schedule_task(task.clone())); - } - - // Now we set timestamp to a later point - Timestamp::set_timestamp( - START_BLOCK_TIME.saturating_add(3_600_000_u64), - ); - - let price_target2 = 1000; - let task = Task:: { - owner_id: other_creator.clone(), - task_id: "123-1-1".as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600, - trigger_function: "lt".as_bytes().to_vec(), - trigger_params: vec![price_target2], - action: Action::XCMP { - destination, - schedule_fee, - execution_fee, - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - get_xcmp_funds(other_creator.clone()); - assert_ok!(AutomationPrice::validate_and_schedule_task(task)); - - assert_eq!(u128::try_from(Tasks::::iter().count()).unwrap(), expired_task_gen + 1); - - assert_eq!( - // 10 task by creator, 1 task by other_creator - 11, - AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), - "total task count is incorrect" - ); - assert_eq!( - 10, - AutomationPrice::get_account_stat(creator.clone(), StatType::TotalTasksPerAccount) - .map_or(0, |v| v), - "total task count is incorrect" - ); - assert_eq!( - 1, - AutomationPrice::get_account_stat( - other_creator.clone(), - StatType::TotalTasksPerAccount - ) - .map_or(0, |v| v), - "total task count is incorrect" - ); - - assert_eq!( - 10, - AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )) - .map_or(0, |v| v.get(&price_target1).unwrap().iter().len()) - ); - - assert_eq!( - 1, - AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "lt".as_bytes().to_vec(), - )) - .map_or(0, |v| v.get(&price_target2).unwrap().iter().len()) - ); - - assert_eq!( - 10, - AutomationPrice::get_sorted_tasks_by_expiration() - .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800)) - .expect("missing task expiration shard") - .len(), - ); - - // now we will sweep, passing a weight limit. In actualy code, this will be the - // remaining_weight in on_idle block - let remain_weight = 100_000_000_000; - AutomationPrice::sweep_expired_task(Weight::from_parts(remain_weight, 0)); - - assert_eq!( - AutomationPrice::get_sorted_tasks_by_expiration() - .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800)), - None - ); - - for i in 0..expired_task_gen { - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskSweep { - owner_id: creator.clone(), - task_id: format!("123-0-{:?}", i).as_bytes().to_vec(), - condition: crate::TaskCondition::AlreadyExpired { - expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800, - now: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - }, - })); - } - - // After sweep there should only one task remain in queue - assert_eq!(Tasks::::iter().count(), 1); - - // The task should be removed from the SortedTasksIndex - assert_eq!( - 0, - AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )) - .expect("missing tasks sorted by price data") - .get(&price_target1) - .map_or(0, |v| v.iter().len()) - ); - // The task should be removed from the SortedTasksIndex - assert_eq!( - 1, - AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "lt".as_bytes().to_vec(), - )) - .expect("missing tasks sorted by price data") - .get(&price_target2) - .map_or(0, |v| v.iter().len()) - ); - - // The task stat should be changed - assert_eq!( - 1, - AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), - "total task count is incorrect" - ); - assert_eq!( - 1, - AutomationPrice::get_account_stat(other_creator, StatType::TotalTasksPerAccount) - .map_or(0, |v| v), - "total task count is incorrect" - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let creator = AccountId32::new(ALICE); + let other_creator = AccountId32::new(BOB); + let para_id: u32 = 1000; + + setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); + + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: MOCK_XCMP_FEE, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + + let expired_task_gen = 10; + let price_target1 = 2000; + for i in 0..expired_task_gen { + // schedule task that has expired + get_xcmp_funds(creator.clone()); + let task = Task:: { + owner_id: creator.clone(), + task_id: format!("123-0-{:?}", i).as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800, + trigger_function: "gt".as_bytes().to_vec(), + trigger_params: vec![price_target1], + action: Action::XCMP { + destination: destination.clone(), + schedule_fee: schedule_fee.clone(), + execution_fee: execution_fee.clone(), + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + assert_ok!(AutomationPrice::validate_and_schedule_task(task.clone())); + } + + // Now we set timestamp to a later point + Timestamp::set_timestamp(START_BLOCK_TIME.saturating_add(3_600_000_u64)); + + let price_target2 = 1000; + let task = Task:: { + owner_id: other_creator.clone(), + task_id: "123-1-1".as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600, + trigger_function: "lt".as_bytes().to_vec(), + trigger_params: vec![price_target2], + action: Action::XCMP { + destination, + schedule_fee, + execution_fee, + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + get_xcmp_funds(other_creator.clone()); + assert_ok!(AutomationPrice::validate_and_schedule_task(task)); + + assert_eq!( + u128::try_from(Tasks::::iter().count()).unwrap(), + expired_task_gen + 1 + ); + + assert_eq!( + // 10 task by creator, 1 task by other_creator + 11, + AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), + "total task count is incorrect" + ); + assert_eq!( + 10, + AutomationPrice::get_account_stat(creator.clone(), StatType::TotalTasksPerAccount) + .map_or(0, |v| v), + "total task count is incorrect" + ); + assert_eq!( + 1, + AutomationPrice::get_account_stat( + other_creator.clone(), + StatType::TotalTasksPerAccount + ) + .map_or(0, |v| v), + "total task count is incorrect" + ); + + assert_eq!( + 10, + AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )) + .map_or(0, |v| v.get(&price_target1).unwrap().iter().len()) + ); + + assert_eq!( + 1, + AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "lt".as_bytes().to_vec(), + )) + .map_or(0, |v| v.get(&price_target2).unwrap().iter().len()) + ); + + assert_eq!( + 10, + AutomationPrice::get_sorted_tasks_by_expiration() + .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800)) + .expect("missing task expiration shard") + .len(), + ); + + // now we will sweep, passing a weight limit. In actualy code, this will be the + // remaining_weight in on_idle block + let remain_weight = 100_000_000_000; + AutomationPrice::sweep_expired_task(Weight::from_parts(remain_weight, 0)); + + assert_eq!( + AutomationPrice::get_sorted_tasks_by_expiration() + .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800)), + None + ); + + for i in 0..expired_task_gen { + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskSweep { + owner_id: creator.clone(), + task_id: format!("123-0-{:?}", i).as_bytes().to_vec(), + condition: crate::TaskCondition::AlreadyExpired { + expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND - 1800, + now: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + }, + })); + } + + // After sweep there should only one task remain in queue + assert_eq!(Tasks::::iter().count(), 1); + + // The task should be removed from the SortedTasksIndex + assert_eq!( + 0, + AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )) + .expect("missing tasks sorted by price data") + .get(&price_target1) + .map_or(0, |v| v.iter().len()) + ); + // The task should be removed from the SortedTasksIndex + assert_eq!( + 1, + AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "lt".as_bytes().to_vec(), + )) + .expect("missing tasks sorted by price data") + .get(&price_target2) + .map_or(0, |v| v.iter().len()) + ); + + // The task stat should be changed + assert_eq!( + 1, + AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), + "total task count is incorrect" + ); + assert_eq!( + 1, + AutomationPrice::get_account_stat(other_creator, StatType::TotalTasksPerAccount) + .map_or(0, |v| v), + "total task count is incorrect" + ); + }) } // Test swap partially data, and leave the rest of sorted index remain intact #[test] fn test_sweep_expired_task_partially() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let creator = AccountId32::new(ALICE); - let _other_creator = AccountId32::new(BOB); - let para_id: u32 = 1000; - - setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: MOCK_XCMP_FEE, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - - let expired_task_gen = 11; - let price_target1 = 2000; - for i in 1..expired_task_gen { - // schedule task that has expired - get_xcmp_funds(creator.clone()); - let task = Task:: { - owner_id: creator.clone(), - task_id: format!("123-0-{:?}", i).as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 10 + i * 10, - trigger_function: "gt".as_bytes().to_vec(), - trigger_params: vec![price_target1], - action: Action::XCMP { - destination: destination.clone(), - schedule_fee: schedule_fee.clone(), - execution_fee: execution_fee.clone(), - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - assert_ok!(AutomationPrice::validate_and_schedule_task(task.clone())); - } - - // Now we set timestamp to a later point - Timestamp::set_timestamp( - START_BLOCK_TIME.saturating_add((3600 + 6 * 10) * 1000), - ); - - assert_eq!( - 10, - AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )) - .map_or(0, |v| v.get(&price_target1).unwrap().iter().len()) - ); - - assert_eq!(10, AutomationPrice::get_sorted_tasks_by_expiration().len()); - - // remaining_weight in on_idle block - let remain_weight = 100_000_000_000; - AutomationPrice::sweep_expired_task(Weight::from_parts(remain_weight, 0)); - - // The task should be removed from the SortedTasksIndex - assert_eq!( - 5, - AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )) - .expect("missing tasks sorted by price data") - .get(&price_target1) - .map_or(0, |v| v.iter().len()) - ); - - // these task all get sweeo - for i in 1..5 { - assert_eq!( - 0, - AutomationPrice::get_sorted_tasks_by_expiration() - .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 10 + i * 10)) - .map_or(0, |v| v.len()), - ); - } - - // these slot remian untouch - for i in 6..10 { - assert_eq!( - 1, - AutomationPrice::get_sorted_tasks_by_expiration() - .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 10 + i * 10)) - .map_or(0, |v| v.len()), - ); - } - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let creator = AccountId32::new(ALICE); + let _other_creator = AccountId32::new(BOB); + let para_id: u32 = 1000; + + setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: MOCK_XCMP_FEE, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + + let expired_task_gen = 11; + let price_target1 = 2000; + for i in 1..expired_task_gen { + // schedule task that has expired + get_xcmp_funds(creator.clone()); + let task = Task:: { + owner_id: creator.clone(), + task_id: format!("123-0-{:?}", i).as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 10 + i * 10, + trigger_function: "gt".as_bytes().to_vec(), + trigger_params: vec![price_target1], + action: Action::XCMP { + destination: destination.clone(), + schedule_fee: schedule_fee.clone(), + execution_fee: execution_fee.clone(), + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + assert_ok!(AutomationPrice::validate_and_schedule_task(task.clone())); + } + + // Now we set timestamp to a later point + Timestamp::set_timestamp(START_BLOCK_TIME.saturating_add((3600 + 6 * 10) * 1000)); + + assert_eq!( + 10, + AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )) + .map_or(0, |v| v.get(&price_target1).unwrap().iter().len()) + ); + + assert_eq!(10, AutomationPrice::get_sorted_tasks_by_expiration().len()); + + // remaining_weight in on_idle block + let remain_weight = 100_000_000_000; + AutomationPrice::sweep_expired_task(Weight::from_parts(remain_weight, 0)); + + // The task should be removed from the SortedTasksIndex + assert_eq!( + 5, + AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )) + .expect("missing tasks sorted by price data") + .get(&price_target1) + .map_or(0, |v| v.iter().len()) + ); + + // these task all get sweeo + for i in 1..5 { + assert_eq!( + 0, + AutomationPrice::get_sorted_tasks_by_expiration() + .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 10 + i * 10)) + .map_or(0, |v| v.len()), + ); + } + + // these slot remian untouch + for i in 6..10 { + assert_eq!( + 1, + AutomationPrice::get_sorted_tasks_by_expiration() + .get(&(START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 10 + i * 10)) + .map_or(0, |v| v.len()), + ); + } + }) } #[test] fn test_schedule_return_error_when_reaching_max_tasks_overall_limit() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let para_id: u32 = 1000; - let creator = AccountId32::new(ALICE); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_asset(&creator, CHAIN1.to_vec()); - - TaskStats::::insert(StatType::TotalTasksOverall, 1_000_000_000); - - assert_noop!( - AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600, - "gt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: 10000000000000 - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - ), - Error::::MaxTasksReached, - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let para_id: u32 = 1000; + let creator = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_asset(&creator, CHAIN1.to_vec()); + + TaskStats::::insert(StatType::TotalTasksOverall, 1_000_000_000); + + assert_noop!( + AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 3600, + "gt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: 10000000000000 + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + ), + Error::::MaxTasksReached, + ); + }) } #[test] fn test_schedule_return_error_when_reaching_max_account_tasks_limit() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let para_id: u32 = 1000; - let creator = AccountId32::new(ALICE); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_asset(&creator, CHAIN1.to_vec()); - - AccountStats::::insert(creator.clone(), StatType::TotalTasksPerAccount, 1_000); - - assert_noop!( - AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(100), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - ), - Error::::MaxTasksPerAccountReached, - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let para_id: u32 = 1000; + let creator = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_asset(&creator, CHAIN1.to_vec()); + + AccountStats::::insert(creator.clone(), StatType::TotalTasksPerAccount, 1_000); + + assert_noop!( + AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(100), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + ), + Error::::MaxTasksPerAccountReached, + ); + }) } // Test when price moves, the TaskQueue will be populated with the right task id @@ -924,700 +935,714 @@ fn test_schedule_return_error_when_reaching_max_account_tasks_limit() { // trigger #[test] fn test_shift_tasks_movement_through_price_changes() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - // TODO: Setup fund once we add fund check and weight - let para_id: u32 = 1000; - let creator = AccountId32::new(ALICE); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); - - let base_price = 10_000_u128; - - // Lets setup 3 tasks - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator.clone()), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(base_price + 1000), - Box::new(destination.clone().into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call.clone(), - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator.clone()), - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - ASSET2.to_vec(), - ASSET3.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(base_price + 900), - Box::new(destination.clone().into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call.clone(), - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator.clone()), - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET3.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 6000, - "gt".as_bytes().to_vec(), - vec!(base_price + 1000), - Box::new(destination.clone().into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call.clone(), - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - - let task_ids = get_task_ids_from_events(); - let task_id1 = task_ids.get(task_ids.len().wrapping_sub(3)).unwrap(); - // let _task_id2 = task_ids.get(task_ids.len().wrapping_sub(2)).unwrap(); - let task_id3 = task_ids.get(task_ids.len().wrapping_sub(1)).unwrap(); - - // at this moment our task queue is empty - // There is schedule tasks, but no tasks in the queue at this moment, because shift_tasks - // has not run yet - assert!(AutomationPrice::get_task_queue().is_empty()); - - // shift_tasks move task from registry to the queue - // At this moment, The price doesn't match the target so there is no change in our tasks - AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); - assert!(AutomationPrice::get_task_queue().is_empty()); - let sorted_task_index = AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )); - assert_eq!(sorted_task_index.map_or_else(|| 0, |x| x.len()), 1); - - // now we change price of pair1 to higher than its target price, while keeping pair2/pair3 low enough, - // only task_id1 will be moved to the queue. - // The target price for those respectively tasks are 10100, 10900, 102000 in their pair - // Therefore after running this price update, first task task_id1 are moved into TaskQueue - let new_pair_1_price: u128 = base_price + 2000; - let new_pair_2_price: u128 = 10_u128; - let mut new_pair_3_price: u128 = 300_u128; - assert_ok!(AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(creator.clone()), - vec!(CHAIN1.to_vec(), CHAIN2.to_vec(), CHAIN2.to_vec()), - vec!(EXCHANGE1.to_vec(), EXCHANGE1.to_vec(), EXCHANGE1.to_vec()), - vec!(ASSET1.to_vec(), ASSET2.to_vec(), ASSET1.to_vec()), - vec!(ASSET2.to_vec(), ASSET3.to_vec(), ASSET3.to_vec()), - vec!(new_pair_1_price, new_pair_2_price, new_pair_3_price), - vec!(START_BLOCK_TIME as u128, START_BLOCK_TIME as u128, START_BLOCK_TIME as u128), - vec!(1, 2, 3), - )); - AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); - assert_eq!(AutomationPrice::get_task_queue(), vec![(creator.clone(), task_id1.clone())]); - // The task are removed from SortedTasksIndex into the TaskQueue, therefore their length - // decrease to 0 - assert_eq!( - AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )) - .map_or_else(|| 0, |x| x.len()), - 0 - ); - - // now we move target price of pair3 to higher than its target, and will observe that its - // task will be moved to TaskQueue too. - new_pair_3_price = base_price + 2000; - let _ = AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(creator.clone()), - vec![CHAIN2.to_vec()], - vec![EXCHANGE1.to_vec()], - vec![ASSET1.to_vec()], - vec![ASSET3.to_vec()], - vec![new_pair_3_price], - vec![START_BLOCK_TIME as u128], - vec![4], - ); - AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); - assert_eq!( - AutomationPrice::get_task_queue(), - vec![(creator.clone(), task_id1.clone()), (creator.clone(), task_id3.clone())] - ); - // The task are removed from SortedTasksIndex into the TaskQueue, therefore their length - // decrease to 0 - assert_eq!( - AutomationPrice::get_sorted_tasks_index(( - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET3.to_vec()), - "gt".as_bytes().to_vec(), - )) - .map_or_else(|| 0, |x| x.len()), - 0 - ); - - // Now, if a new task come up, and its price target matches the existing price, they will - // be trigger too. - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator.clone()), - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - ASSET2.to_vec(), - ASSET3.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "lt".as_bytes().to_vec(), - // price for this asset is 10 in our last update - vec!(20), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: MOCK_XCMP_FEE - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - // The task is now on the SortedTasksIndex - assert_eq!( - AutomationPrice::get_sorted_tasks_index(( - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - (ASSET2.to_vec(), ASSET3.to_vec()), - "lt".as_bytes().to_vec(), - )) - .map_or_else(|| 0, |x| x.len()), - 1 - ); - - AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); - let task_id4 = { - let task_ids = get_task_ids_from_events(); - task_ids.last().unwrap().clone() - }; - - // Now the task is again, moved into the queue and be removed from SortedTasksIndex - assert_eq!( - AutomationPrice::get_task_queue(), - vec![ - (creator.clone(), task_id1.clone()), - (creator.clone(), task_id3.clone()), - (creator, task_id4) - ] - ); - assert_eq!( - AutomationPrice::get_sorted_tasks_index(( - CHAIN2.to_vec(), - EXCHANGE1.to_vec(), - (ASSET2.to_vec(), ASSET3.to_vec()), - "lt".as_bytes().to_vec(), - )) - .map_or_else(|| 0, |x| x.len()), - 0 - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + // TODO: Setup fund once we add fund check and weight + let para_id: u32 = 1000; + let creator = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); + + let base_price = 10_000_u128; + + // Lets setup 3 tasks + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator.clone()), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(base_price + 1000), + Box::new(destination.clone().into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call.clone(), + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator.clone()), + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + ASSET2.to_vec(), + ASSET3.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(base_price + 900), + Box::new(destination.clone().into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call.clone(), + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator.clone()), + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET3.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND + 6000, + "gt".as_bytes().to_vec(), + vec!(base_price + 1000), + Box::new(destination.clone().into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call.clone(), + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + + let task_ids = get_task_ids_from_events(); + let task_id1 = task_ids.get(task_ids.len().wrapping_sub(3)).unwrap(); + // let _task_id2 = task_ids.get(task_ids.len().wrapping_sub(2)).unwrap(); + let task_id3 = task_ids.get(task_ids.len().wrapping_sub(1)).unwrap(); + + // at this moment our task queue is empty + // There is schedule tasks, but no tasks in the queue at this moment, because shift_tasks + // has not run yet + assert!(AutomationPrice::get_task_queue().is_empty()); + + // shift_tasks move task from registry to the queue + // At this moment, The price doesn't match the target so there is no change in our tasks + AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); + assert!(AutomationPrice::get_task_queue().is_empty()); + let sorted_task_index = AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )); + assert_eq!(sorted_task_index.map_or_else(|| 0, |x| x.len()), 1); + + // now we change price of pair1 to higher than its target price, while keeping pair2/pair3 low enough, + // only task_id1 will be moved to the queue. + // The target price for those respectively tasks are 10100, 10900, 102000 in their pair + // Therefore after running this price update, first task task_id1 are moved into TaskQueue + let new_pair_1_price: u128 = base_price + 2000; + let new_pair_2_price: u128 = 10_u128; + let mut new_pair_3_price: u128 = 300_u128; + assert_ok!(AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(creator.clone()), + vec!(CHAIN1.to_vec(), CHAIN2.to_vec(), CHAIN2.to_vec()), + vec!(EXCHANGE1.to_vec(), EXCHANGE1.to_vec(), EXCHANGE1.to_vec()), + vec!(ASSET1.to_vec(), ASSET2.to_vec(), ASSET1.to_vec()), + vec!(ASSET2.to_vec(), ASSET3.to_vec(), ASSET3.to_vec()), + vec!(new_pair_1_price, new_pair_2_price, new_pair_3_price), + vec!( + START_BLOCK_TIME as u128, + START_BLOCK_TIME as u128, + START_BLOCK_TIME as u128 + ), + vec!(1, 2, 3), + )); + AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); + assert_eq!( + AutomationPrice::get_task_queue(), + vec![(creator.clone(), task_id1.clone())] + ); + // The task are removed from SortedTasksIndex into the TaskQueue, therefore their length + // decrease to 0 + assert_eq!( + AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )) + .map_or_else(|| 0, |x| x.len()), + 0 + ); + + // now we move target price of pair3 to higher than its target, and will observe that its + // task will be moved to TaskQueue too. + new_pair_3_price = base_price + 2000; + let _ = AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(creator.clone()), + vec![CHAIN2.to_vec()], + vec![EXCHANGE1.to_vec()], + vec![ASSET1.to_vec()], + vec![ASSET3.to_vec()], + vec![new_pair_3_price], + vec![START_BLOCK_TIME as u128], + vec![4], + ); + AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); + assert_eq!( + AutomationPrice::get_task_queue(), + vec![ + (creator.clone(), task_id1.clone()), + (creator.clone(), task_id3.clone()) + ] + ); + // The task are removed from SortedTasksIndex into the TaskQueue, therefore their length + // decrease to 0 + assert_eq!( + AutomationPrice::get_sorted_tasks_index(( + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET3.to_vec()), + "gt".as_bytes().to_vec(), + )) + .map_or_else(|| 0, |x| x.len()), + 0 + ); + + // Now, if a new task come up, and its price target matches the existing price, they will + // be trigger too. + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator.clone()), + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + ASSET2.to_vec(), + ASSET3.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "lt".as_bytes().to_vec(), + // price for this asset is 10 in our last update + vec!(20), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: MOCK_XCMP_FEE + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + // The task is now on the SortedTasksIndex + assert_eq!( + AutomationPrice::get_sorted_tasks_index(( + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + (ASSET2.to_vec(), ASSET3.to_vec()), + "lt".as_bytes().to_vec(), + )) + .map_or_else(|| 0, |x| x.len()), + 1 + ); + + AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); + let task_id4 = { + let task_ids = get_task_ids_from_events(); + task_ids.last().unwrap().clone() + }; + + // Now the task is again, moved into the queue and be removed from SortedTasksIndex + assert_eq!( + AutomationPrice::get_task_queue(), + vec![ + (creator.clone(), task_id1.clone()), + (creator.clone(), task_id3.clone()), + (creator, task_id4) + ] + ); + assert_eq!( + AutomationPrice::get_sorted_tasks_index(( + CHAIN2.to_vec(), + EXCHANGE1.to_vec(), + (ASSET2.to_vec(), ASSET3.to_vec()), + "lt".as_bytes().to_vec(), + )) + .map_or_else(|| 0, |x| x.len()), + 0 + ); + }) } // the logic around > or < using include/exclude range to include bound or not, it can be subtle // and error prone to human mistake so this test exist to make sure we catch that edge case. #[test] fn test_gt_task_not_run_when_asset_price_equal_target_price() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - // TODO: Setup fund once we add fund check and weight - let para_id: u32 = 1000; - let creator = AccountId32::new(ALICE); - let call: Vec = vec![2, 4, 5]; - let destination = Location::new(1, Parachain(para_id)); - - setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); - - let base_price = 1_000_u128; - - get_xcmp_funds(creator.clone()); - assert_ok!(AutomationPrice::schedule_xcmp_task( - RuntimeOrigin::signed(creator), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - "gt".as_bytes().to_vec(), - vec!(base_price), - Box::new(destination.into()), - Box::new(NATIVE_LOCATION.into()), - Box::new(AssetPayment { - asset_location: Location::new(0, Here).into(), - amount: 100_000 - }), - call, - Weight::from_parts(100_000, 0), - Weight::from_parts(200_000, 0) - )); - - AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); - // Task shouldn't be move to task queue to trigger, and the task queue should be empty - assert!(AutomationPrice::get_task_queue().is_empty()); - - let sorted_task_index = AutomationPrice::get_sorted_tasks_index(( - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - (ASSET1.to_vec(), ASSET2.to_vec()), - "gt".as_bytes().to_vec(), - )); - assert_eq!(1, sorted_task_index.map_or_else(|| 0, |x| x.len())); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + // TODO: Setup fund once we add fund check and weight + let para_id: u32 = 1000; + let creator = AccountId32::new(ALICE); + let call: Vec = vec![2, 4, 5]; + let destination = Location::new(1, Parachain(para_id)); + + setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); + + let base_price = 1_000_u128; + + get_xcmp_funds(creator.clone()); + assert_ok!(AutomationPrice::schedule_xcmp_task( + RuntimeOrigin::signed(creator), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + "gt".as_bytes().to_vec(), + vec!(base_price), + Box::new(destination.into()), + Box::new(NATIVE_LOCATION.into()), + Box::new(AssetPayment { + asset_location: Location::new(0, Here).into(), + amount: 100_000 + }), + call, + Weight::from_parts(100_000, 0), + Weight::from_parts(200_000, 0) + )); + + AutomationPrice::shift_tasks(Weight::from_parts(1_000_000_000, 0)); + // Task shouldn't be move to task queue to trigger, and the task queue should be empty + assert!(AutomationPrice::get_task_queue().is_empty()); + + let sorted_task_index = AutomationPrice::get_sorted_tasks_index(( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + "gt".as_bytes().to_vec(), + )); + assert_eq!(1, sorted_task_index.map_or_else(|| 0, |x| x.len())); + }) } #[test] fn test_emit_event_when_execute_tasks() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let creator = AccountId32::new(ALICE); - let para_id: u32 = 1000; - - setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); - - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: MOCK_XCMP_FEE, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - - get_xcmp_funds(creator.clone()); - let task = Task:: { - owner_id: creator, - task_id: "123-0-1".as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: (START_BLOCK_TIME + 10000) as u128, - trigger_function: "gt".as_bytes().to_vec(), - trigger_params: vec![123], - action: Action::XCMP { - destination, - schedule_fee, - execution_fee, - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - - let _ = AutomationPrice::validate_and_schedule_task(task.clone()); - - AutomationPrice::run_tasks( - vec![(task.owner_id.clone(), task.task_id.clone())], - 100_000_000_000.into(), - ); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskTriggered { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: crate::TaskCondition::TargetPriceMatched { - chain: task.chain.clone(), - exchange: task.exchange.clone(), - asset_pair: task.asset_pair.clone(), - price: 1000_u128, - }, - })); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExecuted { - owner_id: task.owner_id.clone(), - task_id: task.task_id, - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let creator = AccountId32::new(ALICE); + let para_id: u32 = 1000; + + setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); + + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: MOCK_XCMP_FEE, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + + get_xcmp_funds(creator.clone()); + let task = Task:: { + owner_id: creator, + task_id: "123-0-1".as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: (START_BLOCK_TIME + 10000) as u128, + trigger_function: "gt".as_bytes().to_vec(), + trigger_params: vec![123], + action: Action::XCMP { + destination, + schedule_fee, + execution_fee, + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + + let _ = AutomationPrice::validate_and_schedule_task(task.clone()); + + AutomationPrice::run_tasks( + vec![(task.owner_id.clone(), task.task_id.clone())], + 100_000_000_000.into(), + ); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskTriggered { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: crate::TaskCondition::TargetPriceMatched { + chain: task.chain.clone(), + exchange: task.exchange.clone(), + asset_pair: task.asset_pair.clone(), + price: 1000_u128, + }, + })); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExecuted { + owner_id: task.owner_id.clone(), + task_id: task.task_id, + })); + }) } #[test] fn test_decrease_task_count_when_execute_tasks() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let creator1 = AccountId32::new(ALICE); - let creator2 = AccountId32::new(BOB); - let para_id: u32 = 1000; - - setup_assets_and_prices(&creator1, START_BLOCK_TIME as u128); - - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: MOCK_XCMP_FEE, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - - get_xcmp_funds(creator1.clone()); - let task1 = Task:: { - owner_id: creator1.clone(), - task_id: "123-0-1".as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: (START_BLOCK_TIME + 10000) as u128, - trigger_function: "gt".as_bytes().to_vec(), - trigger_params: vec![123], - action: Action::XCMP { - destination: destination.clone(), - schedule_fee: schedule_fee.clone(), - execution_fee: execution_fee.clone(), - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - - get_xcmp_funds(creator2.clone()); - let task2 = Task:: { - owner_id: creator2.clone(), - task_id: "123-1-1".as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: (START_BLOCK_TIME + 10000) as u128, - trigger_function: "gt".as_bytes().to_vec(), - trigger_params: vec![123], - action: Action::XCMP { - destination, - schedule_fee, - execution_fee, - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - - let _ = AutomationPrice::validate_and_schedule_task(task1.clone()); - let _ = AutomationPrice::validate_and_schedule_task(task2); - - assert_eq!( - 2, - AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), - "total task count is wrong" - ); - assert_eq!( - 1, - AutomationPrice::get_account_stat(creator1.clone(), StatType::TotalTasksPerAccount) - .map_or(0, |v| v), - "total task count is wrong" - ); - assert_eq!( - 1, - AutomationPrice::get_account_stat(creator2, StatType::TotalTasksPerAccount) - .map_or(0, |v| v), - "total task count is wrong" - ); - - AutomationPrice::run_tasks( - vec![(task1.owner_id.clone(), task1.task_id)], - 100_000_000_000.into(), - ); - - assert_eq!( - 1, - AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), - "total task count is wrong" - ); - assert_eq!( - 0, - AutomationPrice::get_account_stat(creator1, StatType::TotalTasksPerAccount) - .map_or(0, |v| v), - "total task count of creator1 is wrong" - ); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let creator1 = AccountId32::new(ALICE); + let creator2 = AccountId32::new(BOB); + let para_id: u32 = 1000; + + setup_assets_and_prices(&creator1, START_BLOCK_TIME as u128); + + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: MOCK_XCMP_FEE, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + + get_xcmp_funds(creator1.clone()); + let task1 = Task:: { + owner_id: creator1.clone(), + task_id: "123-0-1".as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: (START_BLOCK_TIME + 10000) as u128, + trigger_function: "gt".as_bytes().to_vec(), + trigger_params: vec![123], + action: Action::XCMP { + destination: destination.clone(), + schedule_fee: schedule_fee.clone(), + execution_fee: execution_fee.clone(), + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + + get_xcmp_funds(creator2.clone()); + let task2 = Task:: { + owner_id: creator2.clone(), + task_id: "123-1-1".as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: (START_BLOCK_TIME + 10000) as u128, + trigger_function: "gt".as_bytes().to_vec(), + trigger_params: vec![123], + action: Action::XCMP { + destination, + schedule_fee, + execution_fee, + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + + let _ = AutomationPrice::validate_and_schedule_task(task1.clone()); + let _ = AutomationPrice::validate_and_schedule_task(task2); + + assert_eq!( + 2, + AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), + "total task count is wrong" + ); + assert_eq!( + 1, + AutomationPrice::get_account_stat(creator1.clone(), StatType::TotalTasksPerAccount) + .map_or(0, |v| v), + "total task count is wrong" + ); + assert_eq!( + 1, + AutomationPrice::get_account_stat(creator2, StatType::TotalTasksPerAccount) + .map_or(0, |v| v), + "total task count is wrong" + ); + + AutomationPrice::run_tasks( + vec![(task1.owner_id.clone(), task1.task_id)], + 100_000_000_000.into(), + ); + + assert_eq!( + 1, + AutomationPrice::get_task_stat(StatType::TotalTasksOverall).map_or(0, |v| v), + "total task count is wrong" + ); + assert_eq!( + 0, + AutomationPrice::get_account_stat(creator1, StatType::TotalTasksPerAccount) + .map_or(0, |v| v), + "total task count of creator1 is wrong" + ); + }) } // when running a task, if the task is already expired, the execution engine won't run the task, // instead an even TaskExpired is emiited #[test] fn test_expired_task_not_run() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let creator = AccountId32::new(ALICE); - let para_id: u32 = 1000; - - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: MOCK_XCMP_FEE, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - - get_xcmp_funds(creator.clone()); - let task = Task:: { - owner_id: creator, - task_id: "123-0-1".as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - trigger_function: "gt".as_bytes().to_vec(), - trigger_params: vec![123], - action: Action::XCMP { - destination, - schedule_fee, - execution_fee, - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - - let _ = AutomationPrice::validate_and_schedule_task(task.clone()); - - // Moving the clock to simulate the task expiration - Timestamp::set_timestamp( - START_BLOCK_TIME.saturating_add(7_200_000_u64), - ); - AutomationPrice::run_tasks( - vec![(task.owner_id.clone(), task.task_id.clone())], - 100_000_000_000.into(), - ); - - assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskTriggered { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: crate::TaskCondition::TargetPriceMatched { - chain: task.chain.clone(), - exchange: task.exchange.clone(), - asset_pair: task.asset_pair.clone(), - price: 1000_u128, - }, - })); - - assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExecuted { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - })); - - assert_last_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExpired { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: crate::TaskCondition::AlreadyExpired { - expired_at: task.expired_at, - now: START_BLOCK_TIME - .saturating_add(7_200_000_u64) - .checked_div(1000) - .ok_or(ArithmeticError::Overflow) - .expect("blocktime is out of range") as u128, - }, - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let creator = AccountId32::new(ALICE); + let para_id: u32 = 1000; + + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: MOCK_XCMP_FEE, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + + get_xcmp_funds(creator.clone()); + let task = Task:: { + owner_id: creator, + task_id: "123-0-1".as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + trigger_function: "gt".as_bytes().to_vec(), + trigger_params: vec![123], + action: Action::XCMP { + destination, + schedule_fee, + execution_fee, + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + + let _ = AutomationPrice::validate_and_schedule_task(task.clone()); + + // Moving the clock to simulate the task expiration + Timestamp::set_timestamp(START_BLOCK_TIME.saturating_add(7_200_000_u64)); + AutomationPrice::run_tasks( + vec![(task.owner_id.clone(), task.task_id.clone())], + 100_000_000_000.into(), + ); + + assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskTriggered { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: crate::TaskCondition::TargetPriceMatched { + chain: task.chain.clone(), + exchange: task.exchange.clone(), + asset_pair: task.asset_pair.clone(), + price: 1000_u128, + }, + })); + + assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExecuted { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + })); + + assert_last_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExpired { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: crate::TaskCondition::AlreadyExpired { + expired_at: task.expired_at, + now: START_BLOCK_TIME + .saturating_add(7_200_000_u64) + .checked_div(1000) + .ok_or(ArithmeticError::Overflow) + .expect("blocktime is out of range") as u128, + }, + })); + }) } // when running a task, if the price has been moved against the target price, rendering the target // price condition not match anymore. we will skip run #[test] fn test_price_move_against_target_price_skip_run() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let creator = AccountId32::new(ALICE); - let para_id: u32 = 1000; - - setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); - - get_xcmp_funds(creator.clone()); - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: MOCK_XCMP_FEE, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - - let task = Task:: { - owner_id: creator, - task_id: "123-0-1".as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: START_BLOCK_TIME - .checked_div(1000) - .map_or(10000000_u128, |v| v.into()) - .saturating_add(100), - trigger_function: "gt".as_bytes().to_vec(), - // This asset price is set to 1000 - // The task is config to run when price > 2000, and we invoked it directly - // so and we will observe that task won't run due to price doesn't match - // the condition - trigger_params: vec![2000], - action: Action::XCMP { - destination, - schedule_fee, - execution_fee, - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - - let _ = AutomationPrice::validate_and_schedule_task(task.clone()); - - AutomationPrice::run_tasks( - vec![(task.owner_id.clone(), task.task_id.clone())], - 100_000_000_000.into(), - ); - - assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskTriggered { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - condition: crate::TaskCondition::TargetPriceMatched { - chain: task.chain.clone(), - exchange: task.exchange.clone(), - asset_pair: task.asset_pair.clone(), - price: 1000_u128, - }, - })); - - assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExecuted { - owner_id: task.owner_id.clone(), - task_id: task.task_id.clone(), - })); - - assert_last_event(RuntimeEvent::AutomationPrice(crate::Event::PriceAlreadyMoved { - owner_id: task.owner_id.clone(), - task_id: task.task_id, - condition: crate::TaskCondition::PriceAlreadyMoved { - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - price: 1000_u128, - target_price: 2000_u128, - }, - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let creator = AccountId32::new(ALICE); + let para_id: u32 = 1000; + + setup_assets_and_prices(&creator, START_BLOCK_TIME as u128); + + get_xcmp_funds(creator.clone()); + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: MOCK_XCMP_FEE, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + + let task = Task:: { + owner_id: creator, + task_id: "123-0-1".as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: START_BLOCK_TIME + .checked_div(1000) + .map_or(10000000_u128, |v| v.into()) + .saturating_add(100), + trigger_function: "gt".as_bytes().to_vec(), + // This asset price is set to 1000 + // The task is config to run when price > 2000, and we invoked it directly + // so and we will observe that task won't run due to price doesn't match + // the condition + trigger_params: vec![2000], + action: Action::XCMP { + destination, + schedule_fee, + execution_fee, + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + + let _ = AutomationPrice::validate_and_schedule_task(task.clone()); + + AutomationPrice::run_tasks( + vec![(task.owner_id.clone(), task.task_id.clone())], + 100_000_000_000.into(), + ); + + assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskTriggered { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + condition: crate::TaskCondition::TargetPriceMatched { + chain: task.chain.clone(), + exchange: task.exchange.clone(), + asset_pair: task.asset_pair.clone(), + price: 1000_u128, + }, + })); + + assert_no_event(RuntimeEvent::AutomationPrice(crate::Event::TaskExecuted { + owner_id: task.owner_id.clone(), + task_id: task.task_id.clone(), + })); + + assert_last_event(RuntimeEvent::AutomationPrice( + crate::Event::PriceAlreadyMoved { + owner_id: task.owner_id.clone(), + task_id: task.task_id, + condition: crate::TaskCondition::PriceAlreadyMoved { + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + price: 1000_u128, + target_price: 2000_u128, + }, + }, + )); + }) } // When canceling, task is removed from 3 places: #[test] fn test_cancel_task_works() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let creator = AccountId32::new(ALICE); - let para_id: u32 = 1000; - - let destination = Location::new(1, Parachain(para_id)); - let schedule_fee = Location::default(); - let execution_fee = AssetPayment { - asset_location: Location::new(1, Parachain(para_id)).into(), - amount: MOCK_XCMP_FEE, - }; - let encoded_call_weight = Weight::from_parts(100_000, 0); - let overall_weight = Weight::from_parts(200_000, 0); - - get_xcmp_funds(creator.clone()); - let task = Task:: { - owner_id: creator, - task_id: "123-0-1".as_bytes().to_vec(), - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), - expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, - trigger_function: "gt".as_bytes().to_vec(), - trigger_params: vec![123], - action: Action::XCMP { - destination, - schedule_fee, - execution_fee, - encoded_call: vec![1, 2, 3], - encoded_call_weight, - overall_weight, - schedule_as: None, - instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, - }, - }; - let _ = AutomationPrice::validate_and_schedule_task(task.clone()); - - let _ = AutomationPrice::cancel_task( - RuntimeOrigin::signed(task.owner_id.clone()), - task.task_id.clone(), - ); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskCancelled { - owner_id: task.owner_id.clone(), - task_id: task.task_id, - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let creator = AccountId32::new(ALICE); + let para_id: u32 = 1000; + + let destination = Location::new(1, Parachain(para_id)); + let schedule_fee = Location::default(); + let execution_fee = AssetPayment { + asset_location: Location::new(1, Parachain(para_id)).into(), + amount: MOCK_XCMP_FEE, + }; + let encoded_call_weight = Weight::from_parts(100_000, 0); + let overall_weight = Weight::from_parts(200_000, 0); + + get_xcmp_funds(creator.clone()); + let task = Task:: { + owner_id: creator, + task_id: "123-0-1".as_bytes().to_vec(), + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset_pair: (ASSET1.to_vec(), ASSET2.to_vec()), + expired_at: START_BLOCK_TIME_1HOUR_AFTER_IN_SECOND, + trigger_function: "gt".as_bytes().to_vec(), + trigger_params: vec![123], + action: Action::XCMP { + destination, + schedule_fee, + execution_fee, + encoded_call: vec![1, 2, 3], + encoded_call_weight, + overall_weight, + schedule_as: None, + instruction_sequence: InstructionSequence::PayThroughRemoteDerivativeAccount, + }, + }; + let _ = AutomationPrice::validate_and_schedule_task(task.clone()); + + let _ = AutomationPrice::cancel_task( + RuntimeOrigin::signed(task.owner_id.clone()), + task.task_id.clone(), + ); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::TaskCancelled { + owner_id: task.owner_id.clone(), + task_id: task.task_id, + })); + }) } #[test] fn test_delete_asset_ok() { - new_test_ext(START_BLOCK_TIME).execute_with(|| { - let sender = AccountId32::new(ALICE); - let key = (CHAIN1.to_vec(), EXCHANGE1.to_vec(), (ASSET1.to_vec(), ASSET2.to_vec())); - - setup_asset(&sender, CHAIN1.to_vec()); - let _ = AutomationPrice::update_asset_prices( - RuntimeOrigin::signed(sender), - vec![CHAIN1.to_vec()], - vec![EXCHANGE1.to_vec()], - vec![ASSET1.to_vec()], - vec![ASSET2.to_vec()], - vec![6789_u128], - vec![START_BLOCK_TIME as u128], - vec![4], - ); - - assert!(AutomationPrice::get_asset_registry_info(&key).is_some()); - assert!(AutomationPrice::get_asset_price_data(&key).is_some()); - - // Now we delete asset, all the relevant asset metadata and price should be deleted - let _ = AutomationPrice::delete_asset( - RawOrigin::Root.into(), - CHAIN1.to_vec(), - EXCHANGE1.to_vec(), - ASSET1.to_vec(), - ASSET2.to_vec(), - ); - - assert!(AutomationPrice::get_asset_registry_info(&key).is_none()); - assert!(AutomationPrice::get_asset_price_data(&key).is_none()); - - assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetDeleted { - chain: CHAIN1.to_vec(), - exchange: EXCHANGE1.to_vec(), - asset1: ASSET1.to_vec(), - asset2: ASSET2.to_vec(), - })); - }) + new_test_ext(START_BLOCK_TIME).execute_with(|| { + let sender = AccountId32::new(ALICE); + let key = ( + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + (ASSET1.to_vec(), ASSET2.to_vec()), + ); + + setup_asset(&sender, CHAIN1.to_vec()); + let _ = AutomationPrice::update_asset_prices( + RuntimeOrigin::signed(sender), + vec![CHAIN1.to_vec()], + vec![EXCHANGE1.to_vec()], + vec![ASSET1.to_vec()], + vec![ASSET2.to_vec()], + vec![6789_u128], + vec![START_BLOCK_TIME as u128], + vec![4], + ); + + assert!(AutomationPrice::get_asset_registry_info(&key).is_some()); + assert!(AutomationPrice::get_asset_price_data(&key).is_some()); + + // Now we delete asset, all the relevant asset metadata and price should be deleted + let _ = AutomationPrice::delete_asset( + RawOrigin::Root.into(), + CHAIN1.to_vec(), + EXCHANGE1.to_vec(), + ASSET1.to_vec(), + ASSET2.to_vec(), + ); + + assert!(AutomationPrice::get_asset_registry_info(&key).is_none()); + assert!(AutomationPrice::get_asset_price_data(&key).is_none()); + + assert_has_event(RuntimeEvent::AutomationPrice(crate::Event::AssetDeleted { + chain: CHAIN1.to_vec(), + exchange: EXCHANGE1.to_vec(), + asset1: ASSET1.to_vec(), + asset2: ASSET2.to_vec(), + })); + }) } diff --git a/pallets/automation-price/src/trigger.rs b/pallets/automation-price/src/trigger.rs index cd7bad856..ab7affe09 100644 --- a/pallets/automation-price/src/trigger.rs +++ b/pallets/automation-price/src/trigger.rs @@ -17,48 +17,48 @@ use crate::{Config, PriceData, Task}; use sp_std::ops::{ - Bound, - Bound::{Excluded, Included}, + Bound, + Bound::{Excluded, Included}, }; pub const TRIGGER_FUNC_GT: &[u8] = "gt".as_bytes(); pub const TRIGGER_FUNC_LT: &[u8] = "lt".as_bytes(); pub trait PriceConditionMatch { - fn is_price_condition_match(&self, price: &PriceData) -> bool; + fn is_price_condition_match(&self, price: &PriceData) -> bool; } impl PriceConditionMatch for Task { - /// check that the task has its condition match the target price of asset - /// - /// # Argument - /// - /// * `price` - the desire price of the asset to check on - fn is_price_condition_match(&self, price: &PriceData) -> bool { - // trigger when target price > current price of the asset - // Example: - // - current price: 100, the task is has target price: 50 -> runable - // - current price: 100, the task is has target price: 150 -> not runable - // + /// check that the task has its condition match the target price of asset + /// + /// # Argument + /// + /// * `price` - the desire price of the asset to check on + fn is_price_condition_match(&self, price: &PriceData) -> bool { + // trigger when target price > current price of the asset + // Example: + // - current price: 100, the task is has target price: 50 -> runable + // - current price: 100, the task is has target price: 150 -> not runable + // - if self.trigger_function == TRIGGER_FUNC_GT.to_vec() { - price.value > self.trigger_params[0] - } else { - price.value < self.trigger_params[0] - } - } + if self.trigger_function == TRIGGER_FUNC_GT.to_vec() { + price.value > self.trigger_params[0] + } else { + price.value < self.trigger_params[0] + } + } } /// Given a condition, and a target price, generate a range that match the condition pub fn range_by_trigger_func( - trigger_func: &[u8], - current_price: &PriceData, + trigger_func: &[u8], + current_price: &PriceData, ) -> (Bound, Bound) { - //Eg sell order, sell when price > - if trigger_func == TRIGGER_FUNC_GT { - (Excluded(u128::MIN), Excluded(current_price.value)) - } else { - // Eg buy order, buy when price < target - (Included(current_price.value), Excluded(u128::MAX)) - } + //Eg sell order, sell when price > + if trigger_func == TRIGGER_FUNC_GT { + (Excluded(u128::MIN), Excluded(current_price.value)) + } else { + // Eg buy order, buy when price < target + (Included(current_price.value), Excluded(u128::MAX)) + } } diff --git a/pallets/automation-price/src/types.rs b/pallets/automation-price/src/types.rs index b3cd15a12..309036688 100644 --- a/pallets/automation-price/src/types.rs +++ b/pallets/automation-price/src/types.rs @@ -25,45 +25,45 @@ use staging_xcm::{latest::prelude::*, VersionedLocation}; /// The struct that stores execution payment for a task. #[derive(Debug, Encode, Eq, PartialEq, Decode, TypeInfo, Clone)] pub struct AssetPayment { - pub asset_location: VersionedLocation, - pub amount: u128, + pub asset_location: VersionedLocation, + pub amount: u128, } /// The enum that stores all action specific data. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)] #[scale_info(skip_type_params(T))] pub enum Action { - XCMP { - destination: Location, - schedule_fee: Location, - execution_fee: AssetPayment, - encoded_call: Vec, - encoded_call_weight: Weight, - overall_weight: Weight, - schedule_as: Option, - instruction_sequence: InstructionSequence, - }, + XCMP { + destination: Location, + schedule_fee: Location, + execution_fee: AssetPayment, + encoded_call: Vec, + encoded_call_weight: Weight, + overall_weight: Weight, + schedule_as: Option, + instruction_sequence: InstructionSequence, + }, } impl Action { - pub fn execution_weight(&self) -> Result { - let weight = match self { - Action::XCMP { .. } => ::WeightInfo::run_xcmp_task(), - }; - Ok(weight.ref_time()) - } + pub fn execution_weight(&self) -> Result { + let weight = match self { + Action::XCMP { .. } => ::WeightInfo::run_xcmp_task(), + }; + Ok(weight.ref_time()) + } - pub fn schedule_fee_location(&self) -> Location { - match self { - Action::XCMP { schedule_fee, .. } => (*schedule_fee).clone(), - } - } + pub fn schedule_fee_location(&self) -> Location { + match self { + Action::XCMP { schedule_fee, .. } => (*schedule_fee).clone(), + } + } } /// The enum represent the type of metric we track #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)] #[scale_info(skip_type_params(T))] pub enum StatType { - TotalTasksOverall, - TotalTasksPerAccount, + TotalTasksOverall, + TotalTasksPerAccount, }