diff --git a/substrate-node/pallets/pallet-smart-contract/src/benchmarking.rs b/substrate-node/pallets/pallet-smart-contract/src/benchmarking.rs index edd6d737e..5c787ee34 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/benchmarking.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/benchmarking.rs @@ -6,6 +6,7 @@ use frame_benchmarking::{account, benchmarks, whitelisted_caller}; use frame_support::{ assert_ok, traits::{OnFinalize, OnInitialize}, + BoundedVec, }; use frame_system::{EventRecord, Pallet as System, RawOrigin}; use pallet_balances::Pallet as Balances; @@ -14,12 +15,15 @@ use pallet_tfgrid::{ CityNameInput, CountryNameInput, DocumentHashInput, DocumentLinkInput, Gw4Input, Ip4Input, LatitudeInput, LongitudeInput, Pallet as TfgridModule, PkInput, RelayInput, ResourcesInput, }; -use pallet_timestamp::Pallet as Timestamp; -use sp_runtime::traits::{Bounded, One, StaticLookup}; +use sp_runtime::{ + traits::{Bounded, One, StaticLookup}, + SaturatedConversion, +}; use sp_std::{ convert::{TryFrom, TryInto}, fmt::Debug, vec, + vec::Vec, }; use tfchain_support::{ resources::Resources, @@ -154,7 +158,7 @@ benchmarks! { let report = types::NruConsumption { contract_id: contract_id, - timestamp: Timestamp::::get().saturated_into::() / 1000, + timestamp: SmartContractModule::::get_current_timestamp_in_secs(), window: 1000, nru: 10 * GIGABYTE, }; @@ -258,9 +262,7 @@ benchmarks! { let solution_provider_id = 1; assert!(SmartContractModule::::solution_providers(solution_provider_id).is_some()); let solution_provider = SmartContractModule::::solution_providers(solution_provider_id).unwrap(); - assert_eq!( - solution_provider.providers, providers - ); + assert_eq!(solution_provider.providers, providers); assert_last_event::(Event::SolutionProviderCreated(solution_provider).into()); } @@ -276,6 +278,7 @@ benchmarks! { verify { assert!(SmartContractModule::::solution_providers(solution_provider_id).is_some()); let solution_provider = SmartContractModule::::solution_providers(solution_provider_id).unwrap(); + assert_eq!(solution_provider.approved, approve); assert_last_event::(Event::SolutionProviderApproved(solution_provider_id, approve).into()); } @@ -292,10 +295,10 @@ benchmarks! { _create_node_contract::(user.clone()); let contract_id = 1; - let now = Timestamp::::get().saturated_into::() / 1000; + let now = SmartContractModule::::get_current_timestamp_in_secs(); let elapsed_seconds = 5; // need to be < 6 secs to bill at same block! let then: u64 = now + elapsed_seconds; - Timestamp::::set_timestamp((then * 1000).try_into().unwrap()); + pallet_timestamp::Pallet::::set_timestamp((then * 1000).try_into().unwrap()); _push_contract_used_resources_report::(farmer.clone()); _push_contract_nru_consumption_report::(farmer.clone(), then, elapsed_seconds); @@ -309,7 +312,7 @@ benchmarks! { assert_eq!(lock.amount_locked, cost); let contract_bill = types::ContractBill { contract_id, - timestamp: >::get().saturated_into::() / 1000, + timestamp: SmartContractModule::::get_current_timestamp_in_secs(), discount_level: types::DiscountLevel::Gold, amount_billed: cost.saturated_into::(), }; @@ -810,7 +813,7 @@ pub(crate) fn get_public_ip_gw_input(public_ip_gw_input: &[u8]) -> Gw4Input { BoundedVec::try_from(public_ip_gw_input.to_vec()).expect("Invalid public ip (gw) input.") } -pub(crate) fn get_deployment_hash_input(deployment_hash_input: &[u8]) -> HexHash { +pub(crate) fn get_deployment_hash_input(deployment_hash_input: &[u8]) -> types::HexHash { deployment_hash_input .to_vec() .try_into() diff --git a/substrate-node/pallets/pallet-smart-contract/src/billing.rs b/substrate-node/pallets/pallet-smart-contract/src/billing.rs new file mode 100644 index 000000000..5e31716dc --- /dev/null +++ b/substrate-node/pallets/pallet-smart-contract/src/billing.rs @@ -0,0 +1,746 @@ +use crate::*; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo}, + ensure, + traits::{Currency, ExistenceRequirement, LockableCurrency, OnUnbalanced, WithdrawReasons}, +}; +use frame_system::offchain::{SendSignedTransaction, SignMessage, Signer}; +use sp_core::Get; +use sp_runtime::{ + traits::{CheckedAdd, CheckedSub, Convert, Zero}, + DispatchResult, Perbill, SaturatedConversion, +}; +use sp_std::vec::Vec; + +impl Pallet { + pub fn bill_conttracts_for_block(block_number: T::BlockNumber) { + // Let offchain worker check if there are contracts on + // billing loop at current index and try to bill them + let index = Self::get_billing_loop_index_from_block_number(block_number); + + let contract_ids = ContractsToBillAt::::get(index); + if contract_ids.is_empty() { + log::info!( + "No contracts to bill at block {:?}, index: {:?}", + block_number, + index + ); + return; + } + + log::info!( + "{:?} contracts to bill at block {:?}", + contract_ids, + block_number + ); + + for contract_id in contract_ids { + if let Some(c) = Contracts::::get(contract_id) { + if let types::ContractData::NodeContract(node_contract) = c.contract_type { + // Is there IP consumption to bill? + let bill_ip = node_contract.public_ips > 0; + + // Is there CU/SU consumption to bill? + // No need for preliminary call to contains_key() because default resource value is empty + let bill_cu_su = !NodeContractResources::::get(contract_id).used.is_empty(); + + // Is there NU consumption to bill? + // No need for preliminary call to contains_key() because default amount_unbilled is 0 + let bill_nu = + ContractBillingInformationByID::::get(contract_id).amount_unbilled > 0; + + // Don't bill if no IP/CU/SU/NU to be billed + if !bill_ip && !bill_cu_su && !bill_nu { + continue; + } + } + } + let _res = Self::bill_contract_using_signed_transaction(contract_id); + } + } + + pub fn bill_contract_using_signed_transaction(contract_id: u64) -> Result<(), Error> { + let signer = Signer::::AuthorityId>::any_account(); + + // Only allow the author of the next block to trigger the billing + Self::is_next_block_author(&signer)?; + + if !signer.can_sign() { + log::error!( + "failed billing contract {:?} account cannot be used to sign transaction", + contract_id, + ); + return Err(>::OffchainSignedTxCannotSign); + } + + let result = + signer.send_signed_transaction(|_acct| Call::bill_contract_for_block { contract_id }); + + if let Some((acc, res)) = result { + // if res is an error this means sending the transaction failed + // this means the transaction was already send before (probably by another node) + // unfortunately the error is always empty (substrate just logs the error and + // returns Err()) + if res.is_err() { + log::error!( + "signed transaction failed for billing contract {:?} using account {:?}", + contract_id, + acc.id + ); + return Err(>::OffchainSignedTxAlreadySent); + } + return Ok(()); + } + log::error!("No local account available"); + return Err(>::OffchainSignedTxNoLocalAccountAvailable); + } + + // Bills a contract (NodeContract, NameContract or RentContract) + // Calculates how much TFT is due by the user and distributes the rewards + pub fn bill_contract(contract_id: u64) -> DispatchResultWithPostInfo { + let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; + + let twin = + pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; + let usable_balance = Self::get_usable_balance(&twin.account_id); + let stash_balance = Self::get_stash_balance(twin.id); + let total_balance = usable_balance + .checked_add(&stash_balance) + .unwrap_or(BalanceOf::::zero()); + + let now = Self::get_current_timestamp_in_secs(); + + // Calculate amount of seconds elapsed based on the contract lock struct + let mut contract_lock = ContractLock::::get(contract.contract_id); + let seconds_elapsed = now.checked_sub(contract_lock.lock_updated).unwrap_or(0); + + // Calculate total amount due + let (regular_amount_due, discount_received) = + contract.calculate_contract_cost_tft(total_balance, seconds_elapsed)?; + let extra_amount_due = match &contract.contract_type { + types::ContractData::RentContract(rc) => { + contract.calculate_extra_fee_cost_tft(rc.node_id, seconds_elapsed)? + } + _ => BalanceOf::::zero(), + }; + let amount_due = regular_amount_due + .checked_add(&extra_amount_due) + .unwrap_or(BalanceOf::::zero()); + + // If there is nothing to be paid and the contract is not in state delete, return + // Can be that the users cancels the contract in the same block that it's getting billed + // where elapsed seconds would be 0, but we still have to distribute rewards + if amount_due == BalanceOf::::zero() && !contract.is_state_delete() { + log::debug!("amount to be billed is 0, nothing to do"); + return Ok(().into()); + }; + + // Calculate total amount locked + let regular_lock_amount = contract_lock + .amount_locked + .checked_add(®ular_amount_due) + .unwrap_or(BalanceOf::::zero()); + let extra_lock_amount = contract_lock + .extra_amount_locked + .checked_add(&extra_amount_due) + .unwrap_or(BalanceOf::::zero()); + let lock_amount = regular_lock_amount + .checked_add(&extra_lock_amount) + .unwrap_or(BalanceOf::::zero()); + + // Handle grace + let contract = Self::handle_grace(&mut contract, usable_balance, lock_amount)?; + + // Only update contract lock in state (Created, GracePeriod) + if !matches!(contract.state, types::ContractState::Deleted(_)) { + // increment cycles billed and update the internal lock struct + contract_lock.lock_updated = now; + contract_lock.cycles += 1; + contract_lock.amount_locked = regular_lock_amount; + contract_lock.extra_amount_locked = extra_lock_amount; + } + + // If still in grace period, no need to continue doing locking and other stuff + if matches!(contract.state, types::ContractState::GracePeriod(_)) { + log::info!("contract {} is still in grace", contract.contract_id); + ContractLock::::insert(contract.contract_id, &contract_lock); + return Ok(().into()); + } + + // Handle contract lock operations + Self::handle_lock(contract, &mut contract_lock, amount_due)?; + + // Always emit a contract billed event + let contract_bill = types::ContractBill { + contract_id: contract.contract_id, + timestamp: Self::get_current_timestamp_in_secs(), + discount_level: discount_received.clone(), + amount_billed: amount_due.saturated_into::(), + }; + Self::deposit_event(Event::ContractBilled(contract_bill)); + + // If the contract is in delete state, remove all associated storage + if matches!(contract.state, types::ContractState::Deleted(_)) { + return Self::remove_contract(contract.contract_id); + } + + // If contract is node contract, set the amount unbilled back to 0 + if matches!(contract.contract_type, types::ContractData::NodeContract(_)) { + let mut contract_billing_info = + ContractBillingInformationByID::::get(contract.contract_id); + contract_billing_info.amount_unbilled = 0; + ContractBillingInformationByID::::insert( + contract.contract_id, + &contract_billing_info, + ); + } + + // Finally update the lock + ContractLock::::insert(contract.contract_id, &contract_lock); + + log::info!("successfully billed contract with id {:?}", contract_id,); + + Ok(().into()) + } + + fn handle_grace( + contract: &mut types::Contract, + usable_balance: BalanceOf, + amount_due: BalanceOf, + ) -> Result<&mut types::Contract, DispatchErrorWithPostInfo> { + let current_block = >::block_number().saturated_into::(); + let node_id = contract.get_node_id(); + + match contract.state { + types::ContractState::GracePeriod(grace_start) => { + // if the usable balance is recharged, we can move the contract to created state again + if usable_balance > amount_due { + Self::update_contract_state(contract, &types::ContractState::Created)?; + Self::deposit_event(Event::ContractGracePeriodEnded { + contract_id: contract.contract_id, + node_id, + twin_id: contract.twin_id, + }); + // If the contract is a rent contract, also move state on associated node contracts + Self::handle_grace_rent_contract(contract, types::ContractState::Created)?; + } else { + let diff = current_block.checked_sub(grace_start).unwrap_or(0); + // If the contract grace period ran out, we can decomission the contract + if diff >= T::GracePeriod::get() { + Self::update_contract_state( + contract, + &types::ContractState::Deleted(types::Cause::OutOfFunds), + )?; + } + } + } + types::ContractState::Created => { + // if the user ran out of funds, move the contract to be in a grace period + // dont lock the tokens because there is nothing to lock + // we can still update the internal contract lock object to figure out later how much was due + // whilst in grace period + if amount_due >= usable_balance { + log::info!( + "Grace period started at block {:?} due to lack of funds", + current_block + ); + Self::update_contract_state( + contract, + &types::ContractState::GracePeriod(current_block), + )?; + // We can't lock the amount due on the contract's lock because the user ran out of funds + Self::deposit_event(Event::ContractGracePeriodStarted { + contract_id: contract.contract_id, + node_id, + twin_id: contract.twin_id, + block_number: current_block.saturated_into(), + }); + // If the contract is a rent contract, also move associated node contract to grace period + Self::handle_grace_rent_contract( + contract, + types::ContractState::GracePeriod(current_block), + )?; + } + } + _ => (), + } + + Ok(contract) + } + + fn handle_grace_rent_contract( + contract: &mut types::Contract, + state: types::ContractState, + ) -> DispatchResultWithPostInfo { + match &contract.contract_type { + types::ContractData::RentContract(rc) => { + let active_node_contracts = ActiveNodeContracts::::get(rc.node_id); + for ctr_id in active_node_contracts { + let mut ctr = + Contracts::::get(ctr_id).ok_or(Error::::ContractNotExists)?; + Self::update_contract_state(&mut ctr, &state)?; + + match state { + types::ContractState::Created => { + Self::deposit_event(Event::ContractGracePeriodEnded { + contract_id: ctr_id, + node_id: rc.node_id, + twin_id: ctr.twin_id, + }); + } + types::ContractState::GracePeriod(block_number) => { + Self::deposit_event(Event::ContractGracePeriodStarted { + contract_id: ctr_id, + node_id: rc.node_id, + twin_id: ctr.twin_id, + block_number, + }); + } + _ => (), + }; + } + } + _ => (), + }; + + Ok(().into()) + } + + fn handle_lock( + contract: &mut types::Contract, + contract_lock: &mut types::ContractLock>, + amount_due: BalanceOf, + ) -> DispatchResultWithPostInfo { + let now = Self::get_current_timestamp_in_secs(); + + // Only lock an amount from the user's balance if the contract is in create state + // The lock is specified on the user's account, since a user can have multiple contracts + // Just extend the lock with the amount due for this contract billing period (lock will be created if not exists) + let twin = + pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; + if matches!(contract.state, types::ContractState::Created) { + let mut locked_balance = Self::get_locked_balance(&twin.account_id); + locked_balance = locked_balance + .checked_add(&amount_due) + .unwrap_or(BalanceOf::::zero()); + ::Currency::extend_lock( + GRID_LOCK_ID, + &twin.account_id, + locked_balance, + WithdrawReasons::all(), + ); + } + + let canceled_and_not_zero = + contract.is_state_delete() && contract_lock.has_some_amount_locked(); + // When the cultivation rewards are ready to be distributed or it's in delete state + // Unlock all reserved balance and distribute + if contract_lock.cycles >= T::DistributionFrequency::get() || canceled_and_not_zero { + // First remove the lock, calculate how much locked balance needs to be unlocked and re-lock the remaining locked balance + let locked_balance = Self::get_locked_balance(&twin.account_id); + let new_locked_balance = + match locked_balance.checked_sub(&contract_lock.total_amount_locked()) { + Some(b) => b, + None => BalanceOf::::zero(), + }; + ::Currency::remove_lock(GRID_LOCK_ID, &twin.account_id); + + // Fetch twin balance, if the amount locked in the contract lock exceeds the current unlocked + // balance we can only transfer out the remaining balance + // https://github.com/threefoldtech/tfchain/issues/479 + let min_balance = ::Currency::minimum_balance(); + let mut twin_balance = match new_locked_balance { + bal if bal > min_balance => { + ::Currency::set_lock( + GRID_LOCK_ID, + &twin.account_id, + new_locked_balance, + WithdrawReasons::all(), + ); + Self::get_usable_balance(&twin.account_id) + } + _ => Self::get_usable_balance(&twin.account_id) + .checked_sub(&min_balance) + .unwrap_or(BalanceOf::::zero()), + }; + + // First, distribute extra cultivation rewards if any + if contract_lock.has_extra_amount_locked() { + log::info!( + "twin balance {:?} contract lock extra amount {:?}", + twin_balance, + contract_lock.extra_amount_locked + ); + + match Self::distribute_extra_cultivation_rewards( + &contract, + twin_balance.min(contract_lock.extra_amount_locked), + ) { + Ok(_) => {} + Err(err) => { + log::error!( + "error while distributing extra cultivation rewards {:?}", + err + ); + return Err(err); + } + }; + + // Update twin balance after distribution + twin_balance = Self::get_usable_balance(&twin.account_id); + } + + log::info!( + "twin balance {:?} contract lock amount {:?}", + twin_balance, + contract_lock.amount_locked + ); + + // Fetch the default pricing policy + let pricing_policy = pallet_tfgrid::PricingPolicies::::get(1) + .ok_or(Error::::PricingPolicyNotExists)?; + + // Then, distribute cultivation rewards + match Self::distribute_cultivation_rewards( + &contract, + &pricing_policy, + twin_balance.min(contract_lock.amount_locked), + ) { + Ok(_) => {} + Err(err) => { + log::error!("error while distributing cultivation rewards {:?}", err); + return Err(err); + } + }; + + // Reset contract lock values + contract_lock.lock_updated = now; + contract_lock.amount_locked = BalanceOf::::zero(); + contract_lock.extra_amount_locked = BalanceOf::::zero(); + contract_lock.cycles = 0; + } + + Ok(().into()) + } + + fn distribute_extra_cultivation_rewards( + contract: &types::Contract, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + log::info!( + "Distributing extra cultivation rewards for contract {:?} with amount {:?}", + contract.contract_id, + amount, + ); + + // If the amount is zero, return + if amount == BalanceOf::::zero() { + return Ok(().into()); + } + + // Fetch source twin = dedicated node user + let src_twin = + pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; + + // Fetch destination twin = farmer + let dst_twin = match &contract.contract_type { + types::ContractData::RentContract(rc) => { + let node = + pallet_tfgrid::Nodes::::get(rc.node_id).ok_or(Error::::NodeNotExists)?; + let farm = pallet_tfgrid::Farms::::get(node.farm_id) + .ok_or(Error::::FarmNotExists)?; + pallet_tfgrid::Twins::::get(farm.twin_id).ok_or(Error::::TwinNotExists)? + } + _ => { + return Err(DispatchErrorWithPostInfo::from( + Error::::InvalidContractType, + )); + } + }; + + // Send 100% to the node's owner (farmer) + log::debug!( + "Transfering: {:?} from contract twin {:?} to farmer account {:?}", + &amount, + &src_twin.account_id, + &dst_twin.account_id, + ); + ::Currency::transfer( + &src_twin.account_id, + &dst_twin.account_id, + amount, + ExistenceRequirement::KeepAlive, + )?; + + Ok(().into()) + } + + // Following: https://library.threefold.me/info/threefold#/tfgrid/farming/threefold__proof_of_utilization + fn distribute_cultivation_rewards( + contract: &types::Contract, + pricing_policy: &pallet_tfgrid::types::PricingPolicy, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + log::info!( + "Distributing cultivation rewards for contract {:?} with amount {:?}", + contract.contract_id, + amount, + ); + + // If the amount is zero, return + if amount == BalanceOf::::zero() { + return Ok(().into()); + } + + // fetch source twin + let twin = + pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; + + // Send 10% to the foundation + let foundation_share = Perbill::from_percent(10) * amount; + log::debug!( + "Transfering: {:?} from contract twin {:?} to foundation account {:?}", + &foundation_share, + &twin.account_id, + &pricing_policy.foundation_account + ); + ::Currency::transfer( + &twin.account_id, + &pricing_policy.foundation_account, + foundation_share, + ExistenceRequirement::KeepAlive, + )?; + + // TODO: send 5% to the staking pool account + let staking_pool_share = Perbill::from_percent(5) * amount; + let staking_pool_account = T::StakingPoolAccount::get(); + log::debug!( + "Transfering: {:?} from contract twin {:?} to staking pool account {:?}", + &staking_pool_share, + &twin.account_id, + &staking_pool_account, + ); + ::Currency::transfer( + &twin.account_id, + &staking_pool_account, + staking_pool_share, + ExistenceRequirement::KeepAlive, + )?; + + let mut sales_share = 50; + + if let Some(provider_id) = contract.solution_provider_id { + if let Some(solution_provider) = SolutionProviders::::get(provider_id) { + let total_take: u8 = solution_provider + .providers + .iter() + .map(|provider| provider.take) + .sum(); + sales_share -= total_take; + + if !solution_provider + .providers + .iter() + .map(|provider| { + let share = Perbill::from_percent(provider.take as u32) * amount; + log::debug!( + "Transfering: {:?} from contract twin {:?} to provider account {:?}", + &share, + &twin.account_id, + &provider.who + ); + ::Currency::transfer( + &twin.account_id, + &provider.who, + share, + ExistenceRequirement::KeepAlive, + ) + }) + .filter(|result| result.is_err()) + .collect::>() + .is_empty() + { + return Err(DispatchErrorWithPostInfo::from( + Error::::InvalidProviderConfiguration, + )); + } + } + }; + + if sales_share > 0 { + let share = Perbill::from_percent(sales_share.into()) * amount; + // Transfer the remaining share to the sales account + // By default it is 50%, if a contract has solution providers it can be less + log::debug!( + "Transfering: {:?} from contract twin {:?} to sales account {:?}", + &share, + &twin.account_id, + &pricing_policy.certified_sales_account + ); + ::Currency::transfer( + &twin.account_id, + &pricing_policy.certified_sales_account, + share, + ExistenceRequirement::KeepAlive, + )?; + } + + // Burn 35%, to not have any imbalance in the system, subtract all previously send amounts with the initial + let amount_to_burn = + (Perbill::from_percent(50) * amount) - foundation_share - staking_pool_share; + + let to_burn = T::Currency::withdraw( + &twin.account_id, + amount_to_burn, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + )?; + + log::debug!( + "Burning: {:?} from contract twin {:?}", + amount_to_burn, + &twin.account_id + ); + T::Burn::on_unbalanced(to_burn); + + Self::deposit_event(Event::TokensBurned { + contract_id: contract.contract_id, + amount: amount_to_burn, + }); + + Ok(().into()) + } + + // Billing index is contract id % (mod) Billing Frequency + // So index belongs to [0; billing_frequency - 1] range + pub fn get_billing_loop_index_from_contract_id(contract_id: u64) -> u64 { + contract_id % BillingFrequency::::get() + } + + // Billing index is block number % (mod) Billing Frequency + // So index belongs to [0; billing_frequency - 1] range + pub fn get_billing_loop_index_from_block_number(block_number: T::BlockNumber) -> u64 { + block_number.saturated_into::() % BillingFrequency::::get() + } + + // Inserts a contract in a billing loop where the index is the contract id % billing frequency + // This way, we don't need to reinsert the contract everytime it gets billed + pub fn insert_contract_in_billing_loop(contract_id: u64) { + let index = Self::get_billing_loop_index_from_contract_id(contract_id); + let mut contract_ids = ContractsToBillAt::::get(index); + + if !contract_ids.contains(&contract_id) { + contract_ids.push(contract_id); + ContractsToBillAt::::insert(index, &contract_ids); + log::debug!( + "Updated contracts after insertion: {:?}, to be billed at index {:?}", + contract_ids, + index + ); + } + } + + // Removes contract from billing loop where the index is the contract id % billing frequency + pub fn remove_contract_from_billing_loop( + contract_id: u64, + ) -> Result<(), DispatchErrorWithPostInfo> { + let index = Self::get_billing_loop_index_from_contract_id(contract_id); + let mut contract_ids = ContractsToBillAt::::get(index); + + ensure!( + contract_ids.contains(&contract_id), + Error::::ContractWrongBillingLoopIndex + ); + + contract_ids.retain(|&c| c != contract_id); + ContractsToBillAt::::insert(index, &contract_ids); + log::debug!( + "Updated contracts after removal: {:?}, to be billed at index {:?}", + contract_ids, + index + ); + + Ok(()) + } + + pub fn _change_billing_frequency(frequency: u64) -> DispatchResultWithPostInfo { + let billing_frequency = BillingFrequency::::get(); + ensure!( + frequency > billing_frequency, + Error::::CanOnlyIncreaseFrequency + ); + + BillingFrequency::::put(frequency); + Self::deposit_event(Event::BillingFrequencyChanged(frequency)); + + Ok(().into()) + } + + // Get the usable balance of an account + // This is the balance minus the minimum balance + pub fn get_usable_balance(account_id: &T::AccountId) -> BalanceOf { + let balance = pallet_balances::pallet::Pallet::::usable_balance(account_id); + let b = balance.saturated_into::(); + BalanceOf::::saturated_from(b) + } + + fn get_locked_balance(account_id: &T::AccountId) -> BalanceOf { + let usable_balance = Self::get_usable_balance(account_id); + let free_balance = ::Currency::free_balance(account_id); + + let locked_balance = free_balance.checked_sub(&usable_balance); + match locked_balance { + Some(balance) => balance, + None => BalanceOf::::zero(), + } + } + + fn get_stash_balance(twin_id: u32) -> BalanceOf { + let account_id = pallet_tfgrid::TwinBoundedAccountID::::get(twin_id); + match account_id { + Some(account) => Self::get_usable_balance(&account), + None => BalanceOf::::zero(), + } + } + + // Validates if the given signer is the next block author based on the validators in session + // This can be used if an extrinsic should be refunded by the author in the same block + // It also requires that the keytype inserted for the offchain workers is the validator key + fn is_next_block_author( + signer: &Signer::AuthorityId>, + ) -> Result<(), Error> { + let author = >::author(); + let validators = >::validators(); + + // Sign some arbitrary data in order to get the AccountId, maybe there is another way to do this? + let signed_message = signer.sign_message(&[0]); + if let Some(signed_message_data) = signed_message { + if let Some(block_author) = author { + let validator = + ::ValidatorIdOf::convert(block_author.clone()) + .ok_or(Error::::IsNotAnAuthority)?; + + let validator_count = validators.len(); + let author_index = (validators.iter().position(|a| a == &validator).unwrap_or(0) + + 1) + % validator_count; + + let signer_validator_account = + ::ValidatorIdOf::convert( + signed_message_data.0.id.clone(), + ) + .ok_or(Error::::IsNotAnAuthority)?; + + if signer_validator_account != validators[author_index] { + return Err(Error::::WrongAuthority); + } + } + } + + Ok(().into()) + } + + pub fn get_current_timestamp_in_secs() -> u64 { + >::get().saturated_into::() / 1000 + } +} diff --git a/substrate-node/pallets/pallet-smart-contract/src/cost.rs b/substrate-node/pallets/pallet-smart-contract/src/cost.rs index b3093de69..c228ad16c 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/cost.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/cost.rs @@ -1,13 +1,6 @@ -use crate::pallet; -use crate::pallet::BalanceOf; -use crate::pallet::Error; -use crate::types; -use crate::types::{Contract, ContractBillingInformation, ServiceContract, ServiceContractBill}; -use crate::Config; -use crate::DedicatedNodesExtraFee; +use crate::*; use frame_support::{dispatch::DispatchErrorWithPostInfo, traits::Get}; use log; -use pallet_tfgrid::types as pallet_tfgrid_types; use sp_runtime::{traits::Zero, Percent, SaturatedConversion}; use substrate_fixed::types::U64F64; use tfchain_support::{ @@ -16,8 +9,8 @@ use tfchain_support::{ types::NodeCertification, }; -impl Contract { - pub fn get_billing_info(&self) -> ContractBillingInformation { +impl types::Contract { + pub fn get_billing_info(&self) -> types::ContractBillingInformation { pallet::ContractBillingInformationByID::::get(self.contract_id) } @@ -36,6 +29,7 @@ impl Contract { // - NameContract let total_cost = self.calculate_contract_cost_units_usd(&pricing_policy, seconds_elapsed)?; + // If cost is 0, reinsert to be billed at next interval if total_cost == 0 { return Ok((BalanceOf::::zero(), types::DiscountLevel::None)); @@ -44,7 +38,7 @@ impl Contract { let total_cost_tft_64 = calculate_cost_in_tft_from_units_usd::(total_cost)?; // Calculate the amount due and discount received based on the total_cost amount due - let (amount_due, discount_received) = calculate_discount::( + let (amount_due, discount_received) = calculate_discount_tft::( total_cost_tft_64, seconds_elapsed, balance, @@ -56,7 +50,7 @@ impl Contract { pub fn calculate_contract_cost_units_usd( &self, - pricing_policy: &pallet_tfgrid_types::PricingPolicy, + pricing_policy: &pallet_tfgrid::types::PricingPolicy, seconds_elapsed: u64, ) -> Result { if seconds_elapsed == 0 { @@ -84,7 +78,7 @@ impl Contract { bill_resources = false } - let contract_cost = calculate_resources_cost::( + let contract_cost = calculate_resources_cost_units_usd::( node_contract_resources.used, node_contract.public_ips, seconds_elapsed, @@ -97,7 +91,7 @@ impl Contract { let node = pallet_tfgrid::Nodes::::get(rent_contract.node_id) .ok_or(Error::::NodeNotExists)?; - let contract_cost = calculate_resources_cost::( + let contract_cost = calculate_resources_cost_units_usd::( node.resources, 0, seconds_elapsed, @@ -135,10 +129,46 @@ impl Contract { } } -impl ServiceContract { +impl types::NruConsumption { + // Calculates the total cost of a report. + // Takes in a report for NRU (network resource units) + // Updates the contract's billing information in storage + pub fn calculate_report_cost_units_usd( + &self, + pricing_policy: &pallet_tfgrid::types::PricingPolicy, + ) { + let mut contract_billing_info = ContractBillingInformationByID::::get(self.contract_id); + if self.timestamp < contract_billing_info.last_updated { + return; + } + + // seconds elapsed is the report.window + let seconds_elapsed = self.window; + log::debug!("seconds elapsed: {:?}", seconds_elapsed); + + // calculate NRU used and the cost + let used_nru = U64F64::from_num(self.nru) / pricing_policy.nu.factor_base_1000(); + let nu_cost = used_nru + * (U64F64::from_num(pricing_policy.nu.value) + / U64F64::from_num(T::BillingReferencePeriod::get())) + * U64F64::from_num(seconds_elapsed); + log::debug!("nu cost: {:?}", nu_cost); + + // save total + let total = nu_cost.round().to_num::(); + log::debug!("total cost: {:?}", total); + + // update contract billing info + contract_billing_info.amount_unbilled += total; + contract_billing_info.last_updated = self.timestamp; + ContractBillingInformationByID::::insert(self.contract_id, &contract_billing_info); + } +} + +impl types::ServiceContract { pub fn calculate_bill_cost_tft( &self, - service_bill: ServiceContractBill, + service_bill: types::ServiceContractBill, ) -> Result, DispatchErrorWithPostInfo> { // Calculate the cost in units usd for service contract bill let total_cost = self.calculate_bill_cost_units_usd::(service_bill); @@ -158,7 +188,7 @@ impl ServiceContract { pub fn calculate_bill_cost_units_usd( &self, - service_bill: ServiceContractBill, + service_bill: types::ServiceContractBill, ) -> u64 { // Convert fees from mUSD to units USD let base_fee_units_usd = self.base_fee * 10000; @@ -173,11 +203,12 @@ impl ServiceContract { } // Calculates the total cost of a node contract. -pub fn calculate_resources_cost( +// https://library.threefold.me/info/threefold#/tfgrid/threefold__cloudunits +pub fn calculate_resources_cost_units_usd( resources: Resources, ipu: u32, seconds_elapsed: u64, - pricing_policy: &pallet_tfgrid_types::PricingPolicy, + pricing_policy: &pallet_tfgrid::types::PricingPolicy, bill_resources: bool, ) -> u64 { let mut total_cost = U64F64::from_num(0); @@ -188,13 +219,14 @@ pub fn calculate_resources_cost( let mru = U64F64::from_num(resources.mru) / pricing_policy.cu.factor_base_1024(); let cru = U64F64::from_num(resources.cru); - let su_used = hru / 1200 + sru / 200; + let su = calculate_su(hru, sru); + // the pricing policy su cost value is expressed in 1 hours or 3600 seconds. // we bill every 3600 seconds but here we need to calculate the cost per second and multiply it by the seconds elapsed. let su_cost = (U64F64::from_num(pricing_policy.su.value) / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(seconds_elapsed) - * su_used; + * su; log::debug!("su cost: {:?}", su_cost); let cu = calculate_cu(cru, mru); @@ -234,47 +266,34 @@ pub fn calculate_extra_fee_cost_units_usd(node_id: u32, seconds_elaps } } -// cu1 = MAX(cru/2, mru/4) -// cu2 = MAX(cru, mru/8) -// cu3 = MAX(cru/4, mru/2) - -// CU = MIN(cu1, cu2, cu3) -pub(crate) fn calculate_cu(cru: U64F64, mru: U64F64) -> U64F64 { - let mru_used_1 = mru / 4; - let cru_used_1 = cru / 2; - let cu1 = if mru_used_1 > cru_used_1 { - mru_used_1 - } else { - cru_used_1 - }; - - let mru_used_2 = mru / 8; - let cru_used_2 = cru; - let cu2 = if mru_used_2 > cru_used_2 { - mru_used_2 - } else { - cru_used_2 - }; +fn calculate_su(hru: U64F64, sru: U64F64) -> U64F64 { + hru / 1200 + sru / 200 +} - let mru_used_3 = mru / 2; - let cru_used_3 = cru / 4; - let cu3 = if mru_used_3 > cru_used_3 { - mru_used_3 - } else { - cru_used_3 - }; +pub fn calculate_cu(cru: U64F64, mru: U64F64) -> U64F64 { + // cu1 = MAX(mru/4, cru/2) + let mru_1 = mru / 4; + let cru_1 = cru / 2; + let cu_1 = mru_1.max(cru_1); - let mut cu = if cu1 > cu2 { cu2 } else { cu1 }; + // cu2 = MAX(mru/8, cru) + let mru_2 = mru / 8; + let cru_2 = cru; + let cu_2 = mru_2.max(cru_2); - cu = if cu > cu3 { cu3 } else { cu }; + // cu3 = MAX(mru/2, cru/4) + let mru_3 = mru / 2; + let cru_3 = cru / 4; + let cu_3 = mru_3.max(cru_3); - cu + // CU = MIN(cu1, cu2, cu3) + cu_1.min(cu_2).min(cu_3) } // Calculates the discount that will be applied to the billing of the contract // Returns an amount due as balance object and a static string indicating which kind of discount it received // (default, bronze, silver, gold or none) -pub fn calculate_discount( +pub fn calculate_discount_tft( amount_due: u64, seconds_elapsed: u64, balance: BalanceOf, diff --git a/substrate-node/pallets/pallet-smart-contract/src/grid_contract.rs b/substrate-node/pallets/pallet-smart-contract/src/grid_contract.rs new file mode 100644 index 000000000..ada8dcaa0 --- /dev/null +++ b/substrate-node/pallets/pallet-smart-contract/src/grid_contract.rs @@ -0,0 +1,766 @@ +use crate::*; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Pays}, + ensure, + pallet_prelude::TypeInfo, + BoundedVec, RuntimeDebugNoBound, +}; +use pallet_tfgrid::pallet::{InterfaceOf, LocationOf, SerialNumberOf, TfgridNode}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use sp_std::{marker::PhantomData, vec, vec::Vec}; +use tfchain_support::{ + traits::{ChangeNode, PublicIpModifier}, + types::PublicIP, +}; + +impl Pallet { + pub fn _create_node_contract( + account_id: T::AccountId, + node_id: u32, + deployment_hash: types::HexHash, + deployment_data: DeploymentDataInput, + public_ips: u32, + solution_provider_id: Option, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; + + let node_power = pallet_tfgrid::NodePower::::get(node_id); + ensure!(!node_power.is_down(), Error::::NodeNotAvailableToDeploy); + + let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; + + // A node is dedicated (can only be used under a rent contract) + // if it has a dedicated node extra fee or if the farm is dedicated + let node_is_dedicated = + DedicatedNodesExtraFee::::get(node_id) > 0 || farm.dedicated_farm; + + // In case there is a rent contract make sure only the contract owner can deploy + // If not, allow to deploy only if node is not dedicated + match ActiveRentContractForNode::::get(node_id) { + Some(contract_id) => { + let rent_contract = + Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; + if rent_contract.twin_id != twin_id { + return Err(Error::::NodeNotAvailableToDeploy.into()); + } + } + None => { + if node_is_dedicated { + return Err(Error::::NodeNotAvailableToDeploy.into()); + } + } + } + + // If the contract with hash and node id exists and it's in any other state then + // contractState::Deleted then we don't allow the creation of it. + // If it exists we allow the user to "restore" this contract + if ContractIDByNodeIDAndHash::::contains_key(node_id, &deployment_hash) { + let contract_id = ContractIDByNodeIDAndHash::::get(node_id, &deployment_hash); + let contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; + if !contract.is_state_delete() { + return Err(Error::::ContractIsNotUnique.into()); + } + } + + let public_ips_list: BoundedVec> = + vec![].try_into().unwrap(); + // Prepare NodeContract struct + let node_contract = types::NodeContract { + node_id, + deployment_hash: deployment_hash.clone(), + deployment_data, + public_ips, + public_ips_list, + }; + + // Create contract + let contract = Self::create_contract( + twin_id, + types::ContractData::NodeContract(node_contract.clone()), + solution_provider_id, + )?; + + let now = Self::get_current_timestamp_in_secs(); + let contract_billing_information = types::ContractBillingInformation { + last_updated: now, + amount_unbilled: 0, + previous_nu_reported: 0, + }; + ContractBillingInformationByID::::insert( + contract.contract_id, + contract_billing_information, + ); + + // Insert contract id by (node_id, hash) + ContractIDByNodeIDAndHash::::insert(node_id, deployment_hash, contract.contract_id); + + // Insert contract into active contracts map + let mut node_contracts = ActiveNodeContracts::::get(&node_contract.node_id); + node_contracts.push(contract.contract_id); + ActiveNodeContracts::::insert(&node_contract.node_id, &node_contracts); + + Self::deposit_event(Event::ContractCreated(contract)); + + Ok(().into()) + } + + pub fn _create_rent_contract( + account_id: T::AccountId, + node_id: u32, + solution_provider_id: Option, + ) -> DispatchResultWithPostInfo { + ensure!( + !ActiveRentContractForNode::::contains_key(node_id), + Error::::NodeHasRentContract + ); + + let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; + ensure!( + pallet_tfgrid::Farms::::contains_key(node.farm_id), + Error::::FarmNotExists + ); + + let node_power = pallet_tfgrid::NodePower::::get(node_id); + ensure!(!node_power.is_down(), Error::::NodeNotAvailableToDeploy); + + let active_node_contracts = ActiveNodeContracts::::get(node_id); + let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; + ensure!( + farm.dedicated_farm || active_node_contracts.is_empty(), + Error::::NodeNotAvailableToDeploy + ); + + // Create contract + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + let contract = Self::create_contract( + twin_id, + types::ContractData::RentContract(types::RentContract { node_id }), + solution_provider_id, + )?; + + // Insert active rent contract for node + ActiveRentContractForNode::::insert(node_id, contract.contract_id); + + Self::deposit_event(Event::ContractCreated(contract)); + + Ok(().into()) + } + + // Registers a DNS name for a Twin + // Ensures uniqueness and also checks if it's a valid DNS name + pub fn _create_name_contract( + source: T::AccountId, + name: Vec, + ) -> DispatchResultWithPostInfo { + ensure!( + pallet_tfgrid::TwinIdByAccountID::::contains_key(&source), + Error::::TwinNotExists + ); + let twin_id = + pallet_tfgrid::TwinIdByAccountID::::get(&source).ok_or(Error::::TwinNotExists)?; + + let valid_name = + NameContractNameOf::::try_from(name).map_err(DispatchErrorWithPostInfo::from)?; + + ensure!( + !ContractIDByNameRegistration::::contains_key(&valid_name), + Error::::NameExists + ); + + let name_contract = types::NameContract { + name: valid_name.clone(), + }; + + let contract = Self::create_contract( + twin_id, + types::ContractData::NameContract(name_contract), + None, + )?; + + ContractIDByNameRegistration::::insert(valid_name, &contract.contract_id); + + Self::deposit_event(Event::ContractCreated(contract)); + + Ok(().into()) + } + + fn create_contract( + twin_id: u32, + mut contract_type: types::ContractData, + solution_provider_id: Option, + ) -> Result, DispatchErrorWithPostInfo> { + // Get the Contract ID map and increment + let mut id = ContractID::::get(); + id = id + 1; + + if let types::ContractData::NodeContract(ref mut nc) = contract_type { + Self::reserve_ip(id, nc)?; + }; + + Self::validate_solution_provider(solution_provider_id)?; + + // Contract is inserted in billing loop ONLY once at contract creation + Self::insert_contract_in_billing_loop(id); + + let contract = types::Contract { + version: CONTRACT_VERSION, + twin_id, + contract_id: id, + state: types::ContractState::Created, + contract_type, + solution_provider_id, + }; + + // insert into contracts map + Contracts::::insert(id, &contract); + + // Update Contract ID + ContractID::::put(id); + + let now = Self::get_current_timestamp_in_secs(); + let mut contract_lock = types::ContractLock::default(); + contract_lock.lock_updated = now; + ContractLock::::insert(id, contract_lock); + + Ok(contract) + } + + pub fn _update_node_contract( + account_id: T::AccountId, + contract_id: u64, + deployment_hash: types::HexHash, + deployment_data: DeploymentDataInput, + ) -> DispatchResultWithPostInfo { + let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; + let twin = + pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; + ensure!( + twin.account_id == account_id, + Error::::TwinNotAuthorizedToUpdateContract + ); + + // Don't allow updates for contracts that are in grace state + let is_grace_state = matches!(contract.state, types::ContractState::GracePeriod(_)); + ensure!( + !is_grace_state, + Error::::CannotUpdateContractInGraceState + ); + + let mut node_contract = Self::get_node_contract(&contract.clone())?; + + // remove and reinsert contract id by node id and hash because that hash can have changed + ContractIDByNodeIDAndHash::::remove( + node_contract.node_id, + node_contract.deployment_hash, + ); + ContractIDByNodeIDAndHash::::insert( + node_contract.node_id, + &deployment_hash, + contract_id, + ); + + node_contract.deployment_hash = deployment_hash; + node_contract.deployment_data = deployment_data; + + // override values + contract.contract_type = types::ContractData::NodeContract(node_contract); + + let state = contract.state.clone(); + Self::update_contract_state(&mut contract, &state)?; + + Self::deposit_event(Event::ContractUpdated(contract)); + + Ok(().into()) + } + + pub fn _cancel_contract( + account_id: T::AccountId, + contract_id: u64, + cause: types::Cause, + ) -> DispatchResultWithPostInfo { + let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; + let twin = + pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; + ensure!( + twin.account_id == account_id, + Error::::TwinNotAuthorizedToCancelContract + ); + + // If it's a rent contract and it still has active workloads, don't allow cancellation. + if matches!( + &contract.contract_type, + types::ContractData::RentContract(_) + ) { + let rent_contract = Self::get_rent_contract(&contract)?; + let active_node_contracts = ActiveNodeContracts::::get(rent_contract.node_id); + ensure!( + active_node_contracts.len() == 0, + Error::::NodeHasActiveContracts + ); + } + + Self::update_contract_state(&mut contract, &types::ContractState::Deleted(cause))?; + Self::bill_contract(contract.contract_id)?; + + Ok(().into()) + } + + pub fn remove_contract(contract_id: u64) -> DispatchResultWithPostInfo { + let contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; + + match contract.contract_type.clone() { + types::ContractData::NodeContract(mut node_contract) => { + if node_contract.public_ips > 0 { + match Self::free_ip(contract_id, &mut node_contract) { + Ok(_) => (), + Err(e) => { + log::info!("error while freeing ips: {:?}", e); + } + } + } + + // remove associated storage items + Self::remove_active_node_contract(node_contract.node_id, contract_id); + ContractIDByNodeIDAndHash::::remove( + node_contract.node_id, + &node_contract.deployment_hash, + ); + NodeContractResources::::remove(contract_id); + ContractBillingInformationByID::::remove(contract_id); + + Self::deposit_event(Event::NodeContractCanceled { + contract_id, + node_id: node_contract.node_id, + twin_id: contract.twin_id, + }); + } + types::ContractData::NameContract(name_contract) => { + ContractIDByNameRegistration::::remove(name_contract.name); + Self::deposit_event(Event::NameContractCanceled { contract_id }); + } + types::ContractData::RentContract(rent_contract) => { + ActiveRentContractForNode::::remove(rent_contract.node_id); + // Remove all associated active node contracts + let active_node_contracts = ActiveNodeContracts::::get(rent_contract.node_id); + for node_contract in active_node_contracts { + Self::remove_contract(node_contract)?; + } + Self::deposit_event(Event::RentContractCanceled { contract_id }); + } + }; + + log::debug!("removing contract"); + Contracts::::remove(contract_id); + ContractLock::::remove(contract_id); + + // Clean up contract from billing loop + // This is the only place it should be done + log::debug!("cleaning up deleted contract from billing loop"); + Self::remove_contract_from_billing_loop(contract_id)?; + + Ok(().into()) + } + + fn remove_active_node_contract(node_id: u32, contract_id: u64) { + let mut contracts = ActiveNodeContracts::::get(&node_id); + + match contracts.iter().position(|id| id == &contract_id) { + Some(index) => { + contracts.remove(index); + } + None => (), + }; + + ActiveNodeContracts::::insert(&node_id, &contracts); + } + + // Helper function that updates the contract state and manages storage accordingly + pub fn update_contract_state( + contract: &mut types::Contract, + state: &types::ContractState, + ) -> DispatchResultWithPostInfo { + // update the state and save the contract + contract.state = state.clone(); + Contracts::::insert(&contract.contract_id, contract.clone()); + + // if the contract is a name contract, nothing to do left here + match contract.contract_type { + types::ContractData::NameContract(_) => return Ok(().into()), + types::ContractData::RentContract(_) => return Ok(().into()), + _ => (), + }; + + // if the contract is a node contract + // manage the ActiveNodeContracts map accordingly + let node_contract = Self::get_node_contract(contract)?; + + let mut contracts = ActiveNodeContracts::::get(&node_contract.node_id); + + match contracts.iter().position(|id| id == &contract.contract_id) { + Some(index) => { + // if the new contract state is delete, remove the contract id from the map + if contract.is_state_delete() { + contracts.remove(index); + } + } + None => { + // if the contract is not present add it to the active contracts map + if state == &types::ContractState::Created { + contracts.push(contract.contract_id); + } + } + }; + + ActiveNodeContracts::::insert(&node_contract.node_id, &contracts); + + Ok(().into()) + } + + pub fn get_node_contract( + contract: &types::Contract, + ) -> Result, DispatchErrorWithPostInfo> { + match contract.contract_type.clone() { + types::ContractData::NodeContract(c) => Ok(c), + _ => { + return Err(DispatchErrorWithPostInfo::from( + Error::::InvalidContractType, + )) + } + } + } + + pub fn get_rent_contract( + contract: &types::Contract, + ) -> Result { + match contract.contract_type.clone() { + types::ContractData::RentContract(c) => Ok(c), + _ => { + return Err(DispatchErrorWithPostInfo::from( + Error::::InvalidContractType, + )) + } + } + } + + fn reserve_ip( + contract_id: u64, + node_contract: &mut types::NodeContract, + ) -> DispatchResultWithPostInfo { + if node_contract.public_ips == 0 { + return Ok(().into()); + } + let node = pallet_tfgrid::Nodes::::get(node_contract.node_id) + .ok_or(Error::::NodeNotExists)?; + + let mut farm = + pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; + + log::debug!( + "Number of farm ips {:?}, number of ips to reserve: {:?}", + farm.public_ips.len(), + node_contract.public_ips as usize + ); + ensure!( + farm.public_ips.len() >= node_contract.public_ips as usize, + Error::::FarmHasNotEnoughPublicIPs + ); + + let mut ips: BoundedVec> = vec![].try_into().unwrap(); + + for i in 0..farm.public_ips.len() { + if ips.len() == node_contract.public_ips as usize { + break; + } + + // if an ip has contract id 0 it means it's not reserved + // reserve it now + if farm.public_ips[i].contract_id == 0 { + let mut ip = farm.public_ips[i].clone(); + ip.contract_id = contract_id; + farm.public_ips[i] = ip.clone(); + ips.try_push(ip).or_else(|_| { + return Err(DispatchErrorWithPostInfo::from( + Error::::FailedToReserveIP, + )); + })?; + } + } + + // Safeguard check if we actually have the amount of ips we wanted to reserve + ensure!( + ips.len() == node_contract.public_ips as usize, + Error::::FarmHasNotEnoughPublicIPsFree + ); + + node_contract.public_ips_list = ips.try_into().or_else(|_| { + return Err(DispatchErrorWithPostInfo::from( + Error::::FailedToReserveIP, + )); + })?; + + // Update the farm with the reserved ips + pallet_tfgrid::Farms::::insert(farm.id, farm); + + Ok(().into()) + } + + fn free_ip( + contract_id: u64, + node_contract: &mut types::NodeContract, + ) -> DispatchResultWithPostInfo { + let node = pallet_tfgrid::Nodes::::get(node_contract.node_id) + .ok_or(Error::::NodeNotExists)?; + + let mut farm = + pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; + + let mut public_ips: BoundedVec> = + vec![].try_into().unwrap(); + for i in 0..farm.public_ips.len() { + // if an ip has contract id 0 it means it's not reserved + // reserve it now + if farm.public_ips[i].contract_id == contract_id { + let mut ip = farm.public_ips[i].clone(); + ip.contract_id = 0; + farm.public_ips[i] = ip.clone(); + public_ips.try_push(ip).or_else(|_| { + return Err(DispatchErrorWithPostInfo::from(Error::::FailedToFreeIPs)); + })?; + } + } + + pallet_tfgrid::Farms::::insert(farm.id, farm); + + // Emit an event containing the IP's freed for this contract + Self::deposit_event(Event::IPsFreed { + contract_id, + public_ips, + }); + + Ok(().into()) + } + + pub fn _report_contract_resources( + source: T::AccountId, + contract_resources: Vec, + ) -> DispatchResultWithPostInfo { + ensure!( + pallet_tfgrid::TwinIdByAccountID::::contains_key(&source), + Error::::TwinNotExists + ); + let twin_id = + pallet_tfgrid::TwinIdByAccountID::::get(&source).ok_or(Error::::TwinNotExists)?; + ensure!( + pallet_tfgrid::NodeIdByTwinID::::contains_key(twin_id), + Error::::NodeNotExists + ); + let node_id = pallet_tfgrid::NodeIdByTwinID::::get(twin_id); + + for contract_resource in contract_resources { + // we know contract exists, fetch it + // if the node is trying to send garbage data we can throw an error here + if let Some(contract) = Contracts::::get(contract_resource.contract_id) { + let node_contract = Self::get_node_contract(&contract)?; + ensure!( + node_contract.node_id == node_id, + Error::::NodeNotAuthorizedToComputeReport + ); + + // Do insert + NodeContractResources::::insert(contract.contract_id, &contract_resource); + + // deposit event + Self::deposit_event(Event::UpdatedUsedResources(contract_resource)); + } + } + + Ok(Pays::No.into()) + } + + pub fn _compute_reports( + source: T::AccountId, + reports: Vec, + ) -> DispatchResultWithPostInfo { + let twin_id = + pallet_tfgrid::TwinIdByAccountID::::get(&source).ok_or(Error::::TwinNotExists)?; + // fetch the node from the source account (signee) + let node_id = pallet_tfgrid::NodeIdByTwinID::::get(&twin_id); + let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; + + let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; + + let pricing_policy = pallet_tfgrid::PricingPolicies::::get(farm.pricing_policy_id) + .ok_or(Error::::PricingPolicyNotExists)?; + + // validation + for report in &reports { + if !Contracts::::contains_key(report.contract_id) { + continue; + } + if !ContractBillingInformationByID::::contains_key(report.contract_id) { + continue; + } + + // we know contract exists, fetch it + // if the node is trying to send garbage data we can throw an error here + let contract = + Contracts::::get(report.contract_id).ok_or(Error::::ContractNotExists)?; + let node_contract = Self::get_node_contract(&contract)?; + ensure!( + node_contract.node_id == node_id, + Error::::NodeNotAuthorizedToComputeReport + ); + + report.calculate_report_cost_units_usd::(&pricing_policy); + + Self::deposit_event(Event::NruConsumptionReportReceived(report.clone())); + } + + Ok(Pays::No.into()) + } + + pub fn _set_dedicated_node_extra_fee( + account_id: T::AccountId, + node_id: u32, + extra_fee: u64, + ) -> DispatchResultWithPostInfo { + // Make sure only the farmer that owns this node can set the extra fee + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; + let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; + ensure!( + twin_id == farm.twin_id, + Error::::UnauthorizedToSetExtraFee + ); + + // Make sure there is no active node or rent contract on this node + ensure!( + ActiveRentContractForNode::::get(node_id).is_none() + && ActiveNodeContracts::::get(&node_id).is_empty(), + Error::::NodeHasActiveContracts + ); + + // Set fee in mUSD + DedicatedNodesExtraFee::::insert(node_id, extra_fee); + Self::deposit_event(Event::NodeExtraFeeSet { node_id, extra_fee }); + + Ok(().into()) + } +} + +impl PublicIpModifier for Pallet { + fn ip_removed(ip: &PublicIP) { + if let Some(mut contract) = Contracts::::get(ip.contract_id) { + match contract.contract_type { + types::ContractData::NodeContract(mut node_contract) => { + if node_contract.public_ips > 0 { + if let Err(e) = Self::free_ip(ip.contract_id, &mut node_contract) { + log::error!("error while freeing ips: {:?}", e); + } + } + contract.contract_type = types::ContractData::NodeContract(node_contract); + + Contracts::::insert(ip.contract_id, &contract); + } + _ => {} + } + } + } +} + +impl ChangeNode, InterfaceOf, SerialNumberOf> for Pallet { + fn node_changed(_node: Option<&TfgridNode>, _new_node: &TfgridNode) {} + + fn node_deleted(node: &TfgridNode) { + // Clean up all active contracts + let active_node_contracts = ActiveNodeContracts::::get(node.id); + for node_contract_id in active_node_contracts { + if let Some(mut contract) = Contracts::::get(node_contract_id) { + // Bill contract + let _ = Self::update_contract_state( + &mut contract, + &types::ContractState::Deleted(types::Cause::CanceledByUser), + ); + let _ = Self::bill_contract(node_contract_id); + } + } + + // First clean up rent contract if it exists + if let Some(rc_id) = ActiveRentContractForNode::::get(node.id) { + if let Some(mut contract) = Contracts::::get(rc_id) { + // Bill contract + let _ = Self::update_contract_state( + &mut contract, + &types::ContractState::Deleted(types::Cause::CanceledByUser), + ); + let _ = Self::bill_contract(contract.contract_id); + } + } + } +} + +/// A Name Contract Name. +#[derive(Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct NameContractName( + pub(crate) BoundedVec, + PhantomData<(T, T::MaxNameContractNameLength)>, +); + +pub const MIN_NAME_LENGTH: u32 = 3; + +impl TryFrom> for NameContractName { + type Error = Error; + + /// Fallible initialization from a provided byte vector if it is below the + /// minimum or exceeds the maximum allowed length or contains invalid ASCII + /// characters. + fn try_from(value: Vec) -> Result { + ensure!( + value.len() >= MIN_NAME_LENGTH as usize, + Self::Error::NameContractNameTooShort + ); + let bounded_vec: BoundedVec = + BoundedVec::try_from(value).map_err(|_| Self::Error::NameContractNameTooLong)?; + ensure!( + is_valid_name_contract_name(&bounded_vec), + Self::Error::NameNotValid + ); + Ok(Self(bounded_vec, PhantomData)) + } +} + +/// Verify that a given slice can be used as a name contract name. +fn is_valid_name_contract_name(input: &[u8]) -> bool { + input + .iter() + .all(|c| matches!(c, b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_')) +} + +impl From> for Vec { + fn from(value: NameContractName) -> Self { + value.0.to_vec() + } +} + +// FIXME: did not find a way to automatically implement this. +impl PartialEq for NameContractName { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for NameContractName {} + +// FIXME: did not find a way to automatically implement this. +impl Clone for NameContractName { + fn clone(&self) -> Self { + Self(self.0.clone(), self.1) + } +} diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index df7992345..1207ff61d 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -1,39 +1,13 @@ #![cfg_attr(not(feature = "std"), no_std)] -use frame_support::{ - dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Pays}, - ensure, - pallet_prelude::DispatchResult, - traits::{ - Currency, EnsureOrigin, ExistenceRequirement, ExistenceRequirement::KeepAlive, Get, - LockableCurrency, OnUnbalanced, WithdrawReasons, - }, - transactional, BoundedVec, -}; -use frame_system::{ - self as system, ensure_signed, - offchain::{AppCrypto, CreateSignedTransaction, SendSignedTransaction, Signer}, -}; -pub use pallet::*; -use pallet_authorship; -use pallet_tfgrid; -use pallet_tfgrid::pallet::{InterfaceOf, LocationOf, SerialNumberOf, TfgridNode}; -use pallet_tfgrid::types as pallet_tfgrid_types; -use pallet_timestamp as timestamp; -use sp_core::crypto::KeyTypeId; -use sp_runtime::{ - traits::{CheckedAdd, CheckedSub, Convert, SaturatedConversion, Zero}, - Perbill, -}; -use sp_std::prelude::*; -use substrate_fixed::types::U64F64; -use system::offchain::SignMessage; -use tfchain_support::{ - traits::{ChangeNode, PublicIpModifier}, - types::PublicIP, -}; - -pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); +pub mod billing; +pub mod cost; +pub mod grid_contract; +pub mod migrations; +pub mod service_contract; +pub mod solution_provider; +pub mod types; +pub mod weights; #[cfg(test)] mod mock; @@ -47,9 +21,11 @@ mod test_utils; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; +// Re-export pallet items so that they can be accessed from the crate namespace. +pub use pallet::*; + pub mod crypto { - use crate::KEY_TYPE; - use sp_core::sr25519::Signature as Sr25519Signature; + use sp_core::{crypto::KeyTypeId, sr25519::Signature as Sr25519Signature}; use sp_runtime::{ app_crypto::{app_crypto, sr25519}, traits::Verify, @@ -57,6 +33,8 @@ pub mod crypto { }; use sp_std::convert::TryFrom; + pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); + app_crypto!(sr25519, KEY_TYPE); pub struct AuthId; @@ -78,21 +56,21 @@ pub mod crypto { } } -pub mod cost; -pub mod migrations; -pub mod name_contract; -pub mod types; -pub mod weights; - #[frame_support::pallet] pub mod pallet { use super::types::*; use super::weights::WeightInfo; use super::*; - use frame_support::pallet_prelude::*; - use frame_support::traits::Hooks; - use frame_support::traits::{Currency, Get, LockIdentifier, LockableCurrency, OnUnbalanced}; - use frame_system::pallet_prelude::*; + use frame_support::{ + pallet_prelude::*, + traits::{Currency, Get, Hooks, LockIdentifier, LockableCurrency, OnUnbalanced}, + }; + use frame_system::{ + self as system, ensure_signed, + offchain::{AppCrypto, CreateSignedTransaction}, + pallet_prelude::*, + }; + use pallet_tfgrid::pallet::{InterfaceOf, LocationOf, SerialNumberOf}; use parity_scale_codec::FullCodec; use sp_core::H256; use sp_std::{ @@ -108,6 +86,7 @@ pub mod pallet { <::Currency as Currency<::AccountId>>::NegativeImbalance; pub const GRID_LOCK_ID: LockIdentifier = *b"gridlock"; + use tfchain_support::types::PublicIP; #[pallet::pallet] #[pallet::without_storage_info] @@ -616,12 +595,8 @@ pub mod pallet { service_contract_id: u64, ) -> DispatchResultWithPostInfo { let account_id = ensure_signed(origin)?; - - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - Self::_service_contract_cancel( - twin_id, + account_id, service_contract_id, types::Cause::CanceledByUser, ) @@ -686,1956 +661,7 @@ pub mod pallet { } fn offchain_worker(block_number: T::BlockNumber) { - // Let offchain worker check if there are contracts on the map at current index - let current_index = Self::get_current_billing_loop_index(); - - let contract_ids = ContractsToBillAt::::get(current_index); - if contract_ids.is_empty() { - log::info!( - "No contracts to bill at block {:?}, index: {:?}", - block_number, - current_index - ); - return; - } - - log::info!( - "{:?} contracts to bill at block {:?}", - contract_ids, - block_number - ); - - for contract_id in contract_ids { - if let Some(c) = Contracts::::get(contract_id) { - if let types::ContractData::NodeContract(node_contract) = c.contract_type { - // Is there IP consumption to bill? - let bill_ip = node_contract.public_ips > 0; - - // Is there CU/SU consumption to bill? - // No need for preliminary call to contains_key() because default resource value is empty - let bill_cu_su = - !NodeContractResources::::get(contract_id).used.is_empty(); - - // Is there NU consumption to bill? - // No need for preliminary call to contains_key() because default amount_unbilled is 0 - let bill_nu = ContractBillingInformationByID::::get(contract_id) - .amount_unbilled - > 0; - - // Don't bill if no IP/CU/SU/NU to be billed - if !bill_ip && !bill_cu_su && !bill_nu { - continue; - } - } - } - let _res = Self::bill_contract_using_signed_transaction(contract_id); - } - } - } -} - -use crate::types::HexHash; -use sp_std::convert::{TryFrom, TryInto}; - -// Internal functions of the pallet -impl Pallet { - pub fn _create_node_contract( - account_id: T::AccountId, - node_id: u32, - deployment_hash: HexHash, - deployment_data: DeploymentDataInput, - public_ips: u32, - solution_provider_id: Option, - ) -> DispatchResultWithPostInfo { - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - - let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; - - let node_power = pallet_tfgrid::NodePower::::get(node_id); - ensure!(!node_power.is_down(), Error::::NodeNotAvailableToDeploy); - - let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; - - // A node is dedicated (can only be used under a rent contract) - // if it has a dedicated node extra fee or if the farm is dedicated - let node_is_dedicated = - DedicatedNodesExtraFee::::get(node_id) > 0 || farm.dedicated_farm; - - // In case there is a rent contract make sure only the contract owner can deploy - // If not, allow to deploy only if node is not dedicated - match ActiveRentContractForNode::::get(node_id) { - Some(contract_id) => { - let rent_contract = - Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - if rent_contract.twin_id != twin_id { - return Err(Error::::NodeNotAvailableToDeploy.into()); - } - } - None => { - if node_is_dedicated { - return Err(Error::::NodeNotAvailableToDeploy.into()); - } - } - } - - // If the contract with hash and node id exists and it's in any other state then - // contractState::Deleted then we don't allow the creation of it. - // If it exists we allow the user to "restore" this contract - if ContractIDByNodeIDAndHash::::contains_key(node_id, &deployment_hash) { - let contract_id = ContractIDByNodeIDAndHash::::get(node_id, &deployment_hash); - let contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - if !contract.is_state_delete() { - return Err(Error::::ContractIsNotUnique.into()); - } - } - - let public_ips_list: BoundedVec> = - vec![].try_into().unwrap(); - // Prepare NodeContract struct - let node_contract = types::NodeContract { - node_id, - deployment_hash: deployment_hash.clone(), - deployment_data, - public_ips, - public_ips_list, - }; - - // Create contract - let contract = Self::_create_contract( - twin_id, - types::ContractData::NodeContract(node_contract.clone()), - solution_provider_id, - )?; - - let now = >::get().saturated_into::() / 1000; - let contract_billing_information = types::ContractBillingInformation { - last_updated: now, - amount_unbilled: 0, - previous_nu_reported: 0, - }; - ContractBillingInformationByID::::insert( - contract.contract_id, - contract_billing_information, - ); - - // Insert contract id by (node_id, hash) - ContractIDByNodeIDAndHash::::insert(node_id, deployment_hash, contract.contract_id); - - // Insert contract into active contracts map - let mut node_contracts = ActiveNodeContracts::::get(&node_contract.node_id); - node_contracts.push(contract.contract_id); - ActiveNodeContracts::::insert(&node_contract.node_id, &node_contracts); - - Self::deposit_event(Event::ContractCreated(contract)); - - Ok(().into()) - } - - pub fn _create_rent_contract( - account_id: T::AccountId, - node_id: u32, - solution_provider_id: Option, - ) -> DispatchResultWithPostInfo { - ensure!( - !ActiveRentContractForNode::::contains_key(node_id), - Error::::NodeHasRentContract - ); - - let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; - ensure!( - pallet_tfgrid::Farms::::contains_key(node.farm_id), - Error::::FarmNotExists - ); - - let node_power = pallet_tfgrid::NodePower::::get(node_id); - ensure!(!node_power.is_down(), Error::::NodeNotAvailableToDeploy); - - let active_node_contracts = ActiveNodeContracts::::get(node_id); - let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; - ensure!( - farm.dedicated_farm || active_node_contracts.is_empty(), - Error::::NodeNotAvailableToDeploy - ); - - // Create contract - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - let contract = Self::_create_contract( - twin_id, - types::ContractData::RentContract(types::RentContract { node_id }), - solution_provider_id, - )?; - - // Insert active rent contract for node - ActiveRentContractForNode::::insert(node_id, contract.contract_id); - - Self::deposit_event(Event::ContractCreated(contract)); - - Ok(().into()) - } - - // Registers a DNS name for a Twin - // Ensures uniqueness and also checks if it's a valid DNS name - pub fn _create_name_contract( - source: T::AccountId, - name: Vec, - ) -> DispatchResultWithPostInfo { - ensure!( - pallet_tfgrid::TwinIdByAccountID::::contains_key(&source), - Error::::TwinNotExists - ); - let twin_id = - pallet_tfgrid::TwinIdByAccountID::::get(&source).ok_or(Error::::TwinNotExists)?; - - let valid_name = - NameContractNameOf::::try_from(name).map_err(DispatchErrorWithPostInfo::from)?; - - // Validate name uniqueness - ensure!( - !ContractIDByNameRegistration::::contains_key(&valid_name), - Error::::NameExists - ); - - let name_contract = types::NameContract { - name: valid_name.clone(), - }; - - let contract = Self::_create_contract( - twin_id, - types::ContractData::NameContract(name_contract), - None, - )?; - - ContractIDByNameRegistration::::insert(valid_name, &contract.contract_id); - - Self::deposit_event(Event::ContractCreated(contract)); - - Ok(().into()) - } - - fn _create_contract( - twin_id: u32, - mut contract_type: types::ContractData, - solution_provider_id: Option, - ) -> Result, DispatchErrorWithPostInfo> { - // Get the Contract ID map and increment - let mut id = ContractID::::get(); - id = id + 1; - - if let types::ContractData::NodeContract(ref mut nc) = contract_type { - Self::_reserve_ip(id, nc)?; - }; - - Self::validate_solution_provider(solution_provider_id)?; - - // Contract is inserted in billing loop ONLY once at contract creation - Self::insert_contract_in_billing_loop(id); - - let contract = types::Contract { - version: CONTRACT_VERSION, - twin_id, - contract_id: id, - state: types::ContractState::Created, - contract_type, - solution_provider_id, - }; - - // insert into contracts map - Contracts::::insert(id, &contract); - - // Update Contract ID - ContractID::::put(id); - - let now = >::get().saturated_into::() / 1000; - let mut contract_lock = types::ContractLock::default(); - contract_lock.lock_updated = now; - ContractLock::::insert(id, contract_lock); - - Ok(contract) - } - - pub fn _update_node_contract( - account_id: T::AccountId, - contract_id: u64, - deployment_hash: HexHash, - deployment_data: DeploymentDataInput, - ) -> DispatchResultWithPostInfo { - let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - let twin = - pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; - ensure!( - twin.account_id == account_id, - Error::::TwinNotAuthorizedToUpdateContract - ); - - // Don't allow updates for contracts that are in grace state - let is_grace_state = matches!(contract.state, types::ContractState::GracePeriod(_)); - ensure!( - !is_grace_state, - Error::::CannotUpdateContractInGraceState - ); - - let mut node_contract = Self::get_node_contract(&contract.clone())?; - - // remove and reinsert contract id by node id and hash because that hash can have changed - ContractIDByNodeIDAndHash::::remove( - node_contract.node_id, - node_contract.deployment_hash, - ); - ContractIDByNodeIDAndHash::::insert( - node_contract.node_id, - &deployment_hash, - contract_id, - ); - - node_contract.deployment_hash = deployment_hash; - node_contract.deployment_data = deployment_data; - - // override values - contract.contract_type = types::ContractData::NodeContract(node_contract); - - let state = contract.state.clone(); - Self::_update_contract_state(&mut contract, &state)?; - - Self::deposit_event(Event::ContractUpdated(contract)); - - Ok(().into()) - } - - pub fn _cancel_contract( - account_id: T::AccountId, - contract_id: u64, - cause: types::Cause, - ) -> DispatchResultWithPostInfo { - let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - let twin = - pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; - ensure!( - twin.account_id == account_id, - Error::::TwinNotAuthorizedToCancelContract - ); - - // If it's a rent contract and it still has active workloads, don't allow cancellation. - if matches!( - &contract.contract_type, - types::ContractData::RentContract(_) - ) { - let rent_contract = Self::get_rent_contract(&contract)?; - let active_node_contracts = ActiveNodeContracts::::get(rent_contract.node_id); - ensure!( - active_node_contracts.len() == 0, - Error::::NodeHasActiveContracts - ); - } - - Self::_update_contract_state(&mut contract, &types::ContractState::Deleted(cause))?; - Self::bill_contract(contract.contract_id)?; - - Ok(().into()) - } - - pub fn _report_contract_resources( - source: T::AccountId, - contract_resources: Vec, - ) -> DispatchResultWithPostInfo { - ensure!( - pallet_tfgrid::TwinIdByAccountID::::contains_key(&source), - Error::::TwinNotExists - ); - let twin_id = - pallet_tfgrid::TwinIdByAccountID::::get(&source).ok_or(Error::::TwinNotExists)?; - ensure!( - pallet_tfgrid::NodeIdByTwinID::::contains_key(twin_id), - Error::::NodeNotExists - ); - let node_id = pallet_tfgrid::NodeIdByTwinID::::get(twin_id); - - for contract_resource in contract_resources { - // we know contract exists, fetch it - // if the node is trying to send garbage data we can throw an error here - if let Some(contract) = Contracts::::get(contract_resource.contract_id) { - let node_contract = Self::get_node_contract(&contract)?; - ensure!( - node_contract.node_id == node_id, - Error::::NodeNotAuthorizedToComputeReport - ); - - // Do insert - NodeContractResources::::insert(contract.contract_id, &contract_resource); - - // deposit event - Self::deposit_event(Event::UpdatedUsedResources(contract_resource)); - } - } - - Ok(Pays::No.into()) - } - - pub fn _compute_reports( - source: T::AccountId, - reports: Vec, - ) -> DispatchResultWithPostInfo { - let twin_id = - pallet_tfgrid::TwinIdByAccountID::::get(&source).ok_or(Error::::TwinNotExists)?; - // fetch the node from the source account (signee) - let node_id = pallet_tfgrid::NodeIdByTwinID::::get(&twin_id); - let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; - - let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; - - let pricing_policy = pallet_tfgrid::PricingPolicies::::get(farm.pricing_policy_id) - .ok_or(Error::::PricingPolicyNotExists)?; - - // validation - for report in &reports { - if !Contracts::::contains_key(report.contract_id) { - continue; - } - if !ContractBillingInformationByID::::contains_key(report.contract_id) { - continue; - } - - // we know contract exists, fetch it - // if the node is trying to send garbage data we can throw an error here - let contract = - Contracts::::get(report.contract_id).ok_or(Error::::ContractNotExists)?; - let node_contract = Self::get_node_contract(&contract)?; - ensure!( - node_contract.node_id == node_id, - Error::::NodeNotAuthorizedToComputeReport - ); - - Self::_calculate_report_cost(&report, &pricing_policy); - Self::deposit_event(Event::NruConsumptionReportReceived(report.clone())); - } - - Ok(Pays::No.into()) - } - - // Calculates the total cost of a report. - // Takes in a report for NRU (network resource units) - // Updates the contract's billing information in storage - pub fn _calculate_report_cost( - report: &types::NruConsumption, - pricing_policy: &pallet_tfgrid_types::PricingPolicy, - ) { - let mut contract_billing_info = - ContractBillingInformationByID::::get(report.contract_id); - if report.timestamp < contract_billing_info.last_updated { - return; - } - - // seconds elapsed is the report.window - let seconds_elapsed = report.window; - log::debug!("seconds elapsed: {:?}", seconds_elapsed); - - // calculate NRU used and the cost - let used_nru = U64F64::from_num(report.nru) / pricing_policy.nu.factor_base_1000(); - let nu_cost = used_nru - * (U64F64::from_num(pricing_policy.nu.value) - / U64F64::from_num(T::BillingReferencePeriod::get())) - * U64F64::from_num(seconds_elapsed); - log::debug!("nu cost: {:?}", nu_cost); - - // save total - let total = nu_cost.round().to_num::(); - log::debug!("total cost: {:?}", total); - - // update contract billing info - contract_billing_info.amount_unbilled += total; - contract_billing_info.last_updated = report.timestamp; - ContractBillingInformationByID::::insert(report.contract_id, &contract_billing_info); - } - - fn bill_contract_using_signed_transaction(contract_id: u64) -> Result<(), Error> { - let signer = Signer::::AuthorityId>::any_account(); - - // Only allow the author of the next block to trigger the billing - Self::is_next_block_author(&signer)?; - - if !signer.can_sign() { - log::error!( - "failed billing contract {:?} account cannot be used to sign transaction", - contract_id, - ); - return Err(>::OffchainSignedTxCannotSign); - } - - let result = - signer.send_signed_transaction(|_acct| Call::bill_contract_for_block { contract_id }); - - if let Some((acc, res)) = result { - // if res is an error this means sending the transaction failed - // this means the transaction was already send before (probably by another node) - // unfortunately the error is always empty (substrate just logs the error and - // returns Err()) - if res.is_err() { - log::error!( - "signed transaction failed for billing contract {:?} using account {:?}", - contract_id, - acc.id - ); - return Err(>::OffchainSignedTxAlreadySent); - } - return Ok(()); - } - log::error!("No local account available"); - return Err(>::OffchainSignedTxNoLocalAccountAvailable); - } - - // Bills a contract (NodeContract, NameContract or RentContract) - // Calculates how much TFT is due by the user and distributes the rewards - fn bill_contract(contract_id: u64) -> DispatchResultWithPostInfo { - let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - - let twin = - pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; - let usable_balance = Self::get_usable_balance(&twin.account_id); - let stash_balance = Self::get_stash_balance(twin.id); - let total_balance = usable_balance - .checked_add(&stash_balance) - .unwrap_or(BalanceOf::::zero()); - - let now = >::get().saturated_into::() / 1000; - - // Calculate amount of seconds elapsed based on the contract lock struct - let mut contract_lock = ContractLock::::get(contract.contract_id); - let seconds_elapsed = now.checked_sub(contract_lock.lock_updated).unwrap_or(0); - - // Calculate total amount due - let (regular_amount_due, discount_received) = - contract.calculate_contract_cost_tft(total_balance, seconds_elapsed)?; - let extra_amount_due = match &contract.contract_type { - types::ContractData::RentContract(rc) => { - contract.calculate_extra_fee_cost_tft(rc.node_id, seconds_elapsed)? - } - _ => BalanceOf::::zero(), - }; - let amount_due = regular_amount_due - .checked_add(&extra_amount_due) - .unwrap_or(BalanceOf::::zero()); - - // If there is nothing to be paid and the contract is not in state delete, return - // Can be that the users cancels the contract in the same block that it's getting billed - // where elapsed seconds would be 0, but we still have to distribute rewards - if amount_due == BalanceOf::::zero() && !contract.is_state_delete() { - log::debug!("amount to be billed is 0, nothing to do"); - return Ok(().into()); - }; - - // Calculate total amount locked - let regular_lock_amount = contract_lock - .amount_locked - .checked_add(®ular_amount_due) - .unwrap_or(BalanceOf::::zero()); - let extra_lock_amount = contract_lock - .extra_amount_locked - .checked_add(&extra_amount_due) - .unwrap_or(BalanceOf::::zero()); - let lock_amount = regular_lock_amount - .checked_add(&extra_lock_amount) - .unwrap_or(BalanceOf::::zero()); - - // Handle grace - let contract = Self::handle_grace(&mut contract, usable_balance, lock_amount)?; - - // Only update contract lock in state (Created, GracePeriod) - if !matches!(contract.state, types::ContractState::Deleted(_)) { - // increment cycles billed and update the internal lock struct - contract_lock.lock_updated = now; - contract_lock.cycles += 1; - contract_lock.amount_locked = regular_lock_amount; - contract_lock.extra_amount_locked = extra_lock_amount; - } - - // If still in grace period, no need to continue doing locking and other stuff - if matches!(contract.state, types::ContractState::GracePeriod(_)) { - log::info!("contract {} is still in grace", contract.contract_id); - ContractLock::::insert(contract.contract_id, &contract_lock); - return Ok(().into()); - } - - // Handle contract lock operations - Self::handle_lock(contract, &mut contract_lock, amount_due)?; - - // Always emit a contract billed event - let contract_bill = types::ContractBill { - contract_id: contract.contract_id, - timestamp: >::get().saturated_into::() / 1000, - discount_level: discount_received.clone(), - amount_billed: amount_due.saturated_into::(), - }; - Self::deposit_event(Event::ContractBilled(contract_bill)); - - // If the contract is in delete state, remove all associated storage - if matches!(contract.state, types::ContractState::Deleted(_)) { - return Self::remove_contract(contract.contract_id); - } - - // If contract is node contract, set the amount unbilled back to 0 - if matches!(contract.contract_type, types::ContractData::NodeContract(_)) { - let mut contract_billing_info = - ContractBillingInformationByID::::get(contract.contract_id); - contract_billing_info.amount_unbilled = 0; - ContractBillingInformationByID::::insert( - contract.contract_id, - &contract_billing_info, - ); - } - - // Finally update the lock - ContractLock::::insert(contract.contract_id, &contract_lock); - - log::info!("successfully billed contract with id {:?}", contract_id,); - - Ok(().into()) - } - - fn handle_grace( - contract: &mut types::Contract, - usable_balance: BalanceOf, - amount_due: BalanceOf, - ) -> Result<&mut types::Contract, DispatchErrorWithPostInfo> { - let current_block = >::block_number().saturated_into::(); - let node_id = contract.get_node_id(); - - match contract.state { - types::ContractState::GracePeriod(grace_start) => { - // if the usable balance is recharged, we can move the contract to created state again - if usable_balance > amount_due { - Self::_update_contract_state(contract, &types::ContractState::Created)?; - Self::deposit_event(Event::ContractGracePeriodEnded { - contract_id: contract.contract_id, - node_id, - twin_id: contract.twin_id, - }); - // If the contract is a rent contract, also move state on associated node contracts - Self::handle_grace_rent_contract(contract, types::ContractState::Created)?; - } else { - let diff = current_block.checked_sub(grace_start).unwrap_or(0); - // If the contract grace period ran out, we can decomission the contract - if diff >= T::GracePeriod::get() { - Self::_update_contract_state( - contract, - &types::ContractState::Deleted(types::Cause::OutOfFunds), - )?; - } - } - } - types::ContractState::Created => { - // if the user ran out of funds, move the contract to be in a grace period - // dont lock the tokens because there is nothing to lock - // we can still update the internal contract lock object to figure out later how much was due - // whilst in grace period - if amount_due >= usable_balance { - log::info!( - "Grace period started at block {:?} due to lack of funds", - current_block - ); - Self::_update_contract_state( - contract, - &types::ContractState::GracePeriod(current_block), - )?; - // We can't lock the amount due on the contract's lock because the user ran out of funds - Self::deposit_event(Event::ContractGracePeriodStarted { - contract_id: contract.contract_id, - node_id, - twin_id: contract.twin_id, - block_number: current_block.saturated_into(), - }); - // If the contract is a rent contract, also move associated node contract to grace period - Self::handle_grace_rent_contract( - contract, - types::ContractState::GracePeriod(current_block), - )?; - } - } - _ => (), - } - - Ok(contract) - } - - fn handle_grace_rent_contract( - contract: &mut types::Contract, - state: types::ContractState, - ) -> DispatchResultWithPostInfo { - match &contract.contract_type { - types::ContractData::RentContract(rc) => { - let active_node_contracts = ActiveNodeContracts::::get(rc.node_id); - for ctr_id in active_node_contracts { - let mut ctr = - Contracts::::get(ctr_id).ok_or(Error::::ContractNotExists)?; - Self::_update_contract_state(&mut ctr, &state)?; - - match state { - types::ContractState::Created => { - Self::deposit_event(Event::ContractGracePeriodEnded { - contract_id: ctr_id, - node_id: rc.node_id, - twin_id: ctr.twin_id, - }); - } - types::ContractState::GracePeriod(block_number) => { - Self::deposit_event(Event::ContractGracePeriodStarted { - contract_id: ctr_id, - node_id: rc.node_id, - twin_id: ctr.twin_id, - block_number, - }); - } - _ => (), - }; - } - } - _ => (), - }; - - Ok(().into()) - } - - fn handle_lock( - contract: &mut types::Contract, - contract_lock: &mut types::ContractLock>, - amount_due: BalanceOf, - ) -> DispatchResultWithPostInfo { - let now = >::get().saturated_into::() / 1000; - - // Only lock an amount from the user's balance if the contract is in create state - // The lock is specified on the user's account, since a user can have multiple contracts - // Just extend the lock with the amount due for this contract billing period (lock will be created if not exists) - let twin = - pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; - if matches!(contract.state, types::ContractState::Created) { - let mut locked_balance = Self::get_locked_balance(&twin.account_id); - locked_balance = locked_balance - .checked_add(&amount_due) - .unwrap_or(BalanceOf::::zero()); - ::Currency::extend_lock( - GRID_LOCK_ID, - &twin.account_id, - locked_balance, - WithdrawReasons::all(), - ); - } - - let canceled_and_not_zero = - contract.is_state_delete() && contract_lock.has_some_amount_locked(); - // When the cultivation rewards are ready to be distributed or it's in delete state - // Unlock all reserved balance and distribute - if contract_lock.cycles >= T::DistributionFrequency::get() || canceled_and_not_zero { - // First remove the lock, calculate how much locked balance needs to be unlocked and re-lock the remaining locked balance - let locked_balance = Self::get_locked_balance(&twin.account_id); - let new_locked_balance = - match locked_balance.checked_sub(&contract_lock.total_amount_locked()) { - Some(b) => b, - None => BalanceOf::::zero(), - }; - ::Currency::remove_lock(GRID_LOCK_ID, &twin.account_id); - - // Fetch twin balance, if the amount locked in the contract lock exceeds the current unlocked - // balance we can only transfer out the remaining balance - // https://github.com/threefoldtech/tfchain/issues/479 - let mut twin_balance = Self::get_usable_balance(&twin.account_id); - - if new_locked_balance > ::Currency::minimum_balance() { - // TODO: check if this is needed - ::Currency::set_lock( - GRID_LOCK_ID, - &twin.account_id, - new_locked_balance, - WithdrawReasons::all(), - ); - twin_balance = Self::get_usable_balance(&twin.account_id); - } else { - twin_balance = twin_balance - .checked_sub(&::Currency::minimum_balance()) - .unwrap_or(BalanceOf::::zero()); - }; - - // First, distribute extra cultivation rewards if any - if contract_lock.has_extra_amount_locked() { - log::info!( - "twin balance {:?} contract lock extra amount {:?}", - twin_balance, - contract_lock.extra_amount_locked - ); - - match Self::_distribute_extra_cultivation_rewards( - &contract, - twin_balance.min(contract_lock.extra_amount_locked), - ) { - Ok(_) => {} - Err(err) => { - log::error!( - "error while distributing extra cultivation rewards {:?}", - err - ); - return Err(err); - } - }; - - // Update twin balance after distribution - twin_balance = Self::get_usable_balance(&twin.account_id); - } - - log::info!( - "twin balance {:?} contract lock amount {:?}", - twin_balance, - contract_lock.amount_locked - ); - - // Fetch the default pricing policy - let pricing_policy = pallet_tfgrid::PricingPolicies::::get(1) - .ok_or(Error::::PricingPolicyNotExists)?; - - // Then, distribute cultivation rewards - match Self::_distribute_cultivation_rewards( - &contract, - &pricing_policy, - twin_balance.min(contract_lock.amount_locked), - ) { - Ok(_) => {} - Err(err) => { - log::error!("error while distributing cultivation rewards {:?}", err); - return Err(err); - } - }; - - // Reset contract lock values - contract_lock.lock_updated = now; - contract_lock.amount_locked = BalanceOf::::zero(); - contract_lock.extra_amount_locked = BalanceOf::::zero(); - contract_lock.cycles = 0; - } - - Ok(().into()) - } - - pub fn remove_contract(contract_id: u64) -> DispatchResultWithPostInfo { - let contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - - match contract.contract_type.clone() { - types::ContractData::NodeContract(mut node_contract) => { - if node_contract.public_ips > 0 { - match Self::_free_ip(contract_id, &mut node_contract) { - Ok(_) => (), - Err(e) => { - log::info!("error while freeing ips: {:?}", e); - } - } - } - - // remove associated storage items - Self::remove_active_node_contract(node_contract.node_id, contract_id); - ContractIDByNodeIDAndHash::::remove( - node_contract.node_id, - &node_contract.deployment_hash, - ); - NodeContractResources::::remove(contract_id); - ContractBillingInformationByID::::remove(contract_id); - - Self::deposit_event(Event::NodeContractCanceled { - contract_id, - node_id: node_contract.node_id, - twin_id: contract.twin_id, - }); - } - types::ContractData::NameContract(name_contract) => { - ContractIDByNameRegistration::::remove(name_contract.name); - Self::deposit_event(Event::NameContractCanceled { contract_id }); - } - types::ContractData::RentContract(rent_contract) => { - ActiveRentContractForNode::::remove(rent_contract.node_id); - // Remove all associated active node contracts - let active_node_contracts = ActiveNodeContracts::::get(rent_contract.node_id); - for node_contract in active_node_contracts { - Self::remove_contract(node_contract)?; - } - Self::deposit_event(Event::RentContractCanceled { contract_id }); - } - }; - - log::debug!("removing contract"); - Contracts::::remove(contract_id); - ContractLock::::remove(contract_id); - - // Clean up contract from billing loop - // This is the only place it should be done - log::debug!("cleaning up deleted contract from billing loop"); - Self::remove_contract_from_billing_loop(contract_id)?; - - Ok(().into()) - } - - fn _distribute_extra_cultivation_rewards( - contract: &types::Contract, - amount: BalanceOf, - ) -> DispatchResultWithPostInfo { - log::info!( - "Distributing extra cultivation rewards for contract {:?} with amount {:?}", - contract.contract_id, - amount, - ); - - // If the amount is zero, return - if amount == BalanceOf::::zero() { - return Ok(().into()); - } - - // Fetch source twin = dedicated node user - let src_twin = - pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; - - // Fetch destination twin = farmer - let dst_twin = match &contract.contract_type { - types::ContractData::RentContract(rc) => { - let node = - pallet_tfgrid::Nodes::::get(rc.node_id).ok_or(Error::::NodeNotExists)?; - let farm = pallet_tfgrid::Farms::::get(node.farm_id) - .ok_or(Error::::FarmNotExists)?; - pallet_tfgrid::Twins::::get(farm.twin_id).ok_or(Error::::TwinNotExists)? - } - _ => { - return Err(DispatchErrorWithPostInfo::from( - Error::::InvalidContractType, - )); - } - }; - - // Send 100% to the node's owner (farmer) - log::debug!( - "Transfering: {:?} from contract twin {:?} to farmer account {:?}", - &amount, - &src_twin.account_id, - &dst_twin.account_id, - ); - ::Currency::transfer( - &src_twin.account_id, - &dst_twin.account_id, - amount, - ExistenceRequirement::KeepAlive, - )?; - - Ok(().into()) - } - - // Following: https://library.threefold.me/info/threefold#/tfgrid/farming/threefold__proof_of_utilization - fn _distribute_cultivation_rewards( - contract: &types::Contract, - pricing_policy: &pallet_tfgrid_types::PricingPolicy, - amount: BalanceOf, - ) -> DispatchResultWithPostInfo { - log::info!( - "Distributing cultivation rewards for contract {:?} with amount {:?}", - contract.contract_id, - amount, - ); - - // If the amount is zero, return - if amount == BalanceOf::::zero() { - return Ok(().into()); + Self::bill_conttracts_for_block(block_number); } - - // fetch source twin - let twin = - pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; - - // Send 10% to the foundation - let foundation_share = Perbill::from_percent(10) * amount; - log::debug!( - "Transfering: {:?} from contract twin {:?} to foundation account {:?}", - &foundation_share, - &twin.account_id, - &pricing_policy.foundation_account - ); - ::Currency::transfer( - &twin.account_id, - &pricing_policy.foundation_account, - foundation_share, - ExistenceRequirement::KeepAlive, - )?; - - // TODO: send 5% to the staking pool account - let staking_pool_share = Perbill::from_percent(5) * amount; - let staking_pool_account = T::StakingPoolAccount::get(); - log::debug!( - "Transfering: {:?} from contract twin {:?} to staking pool account {:?}", - &staking_pool_share, - &twin.account_id, - &staking_pool_account, - ); - ::Currency::transfer( - &twin.account_id, - &staking_pool_account, - staking_pool_share, - ExistenceRequirement::KeepAlive, - )?; - - let mut sales_share = 50; - - if let Some(provider_id) = contract.solution_provider_id { - if let Some(solution_provider) = SolutionProviders::::get(provider_id) { - let total_take: u8 = solution_provider - .providers - .iter() - .map(|provider| provider.take) - .sum(); - sales_share -= total_take; - - if !solution_provider - .providers - .iter() - .map(|provider| { - let share = Perbill::from_percent(provider.take as u32) * amount; - log::debug!( - "Transfering: {:?} from contract twin {:?} to provider account {:?}", - &share, - &twin.account_id, - &provider.who - ); - ::Currency::transfer( - &twin.account_id, - &provider.who, - share, - KeepAlive, - ) - }) - .filter(|result| result.is_err()) - .collect::>() - .is_empty() - { - return Err(DispatchErrorWithPostInfo::from( - Error::::InvalidProviderConfiguration, - )); - } - } - }; - - if sales_share > 0 { - let share = Perbill::from_percent(sales_share.into()) * amount; - // Transfer the remaining share to the sales account - // By default it is 50%, if a contract has solution providers it can be less - log::debug!( - "Transfering: {:?} from contract twin {:?} to sales account {:?}", - &share, - &twin.account_id, - &pricing_policy.certified_sales_account - ); - ::Currency::transfer( - &twin.account_id, - &pricing_policy.certified_sales_account, - share, - KeepAlive, - )?; - } - - // Burn 35%, to not have any imbalance in the system, subtract all previously send amounts with the initial - let amount_to_burn = - (Perbill::from_percent(50) * amount) - foundation_share - staking_pool_share; - - let to_burn = T::Currency::withdraw( - &twin.account_id, - amount_to_burn, - WithdrawReasons::FEE, - ExistenceRequirement::KeepAlive, - )?; - - log::debug!( - "Burning: {:?} from contract twin {:?}", - amount_to_burn, - &twin.account_id - ); - T::Burn::on_unbalanced(to_burn); - - Self::deposit_event(Event::TokensBurned { - contract_id: contract.contract_id, - amount: amount_to_burn, - }); - - Ok(().into()) - } - - // Inserts a contract in a billing loop where the index is the contract id % billing frequency - // This way, we don't need to reinsert the contract everytime it gets billed - pub fn insert_contract_in_billing_loop(contract_id: u64) { - let index = Self::get_contract_billing_loop_index(contract_id); - let mut contract_ids = ContractsToBillAt::::get(index); - - if !contract_ids.contains(&contract_id) { - contract_ids.push(contract_id); - ContractsToBillAt::::insert(index, &contract_ids); - log::debug!( - "Updated contracts after insertion: {:?}, to be billed at index {:?}", - contract_ids, - index - ); - } - } - - // Removes contract from billing loop where the index is the contract id % billing frequency - pub fn remove_contract_from_billing_loop( - contract_id: u64, - ) -> Result<(), DispatchErrorWithPostInfo> { - let index = Self::get_contract_billing_loop_index(contract_id); - let mut contract_ids = ContractsToBillAt::::get(index); - - ensure!( - contract_ids.contains(&contract_id), - Error::::ContractWrongBillingLoopIndex - ); - - contract_ids.retain(|&c| c != contract_id); - ContractsToBillAt::::insert(index, &contract_ids); - log::debug!( - "Updated contracts after removal: {:?}, to be billed at index {:?}", - contract_ids, - index - ); - - Ok(()) - } - - // Helper function that updates the contract state and manages storage accordingly - pub fn _update_contract_state( - contract: &mut types::Contract, - state: &types::ContractState, - ) -> DispatchResultWithPostInfo { - // update the state and save the contract - contract.state = state.clone(); - Contracts::::insert(&contract.contract_id, contract.clone()); - - // if the contract is a name contract, nothing to do left here - match contract.contract_type { - types::ContractData::NameContract(_) => return Ok(().into()), - types::ContractData::RentContract(_) => return Ok(().into()), - _ => (), - }; - - // if the contract is a node contract - // manage the ActiveNodeContracts map accordingly - let node_contract = Self::get_node_contract(contract)?; - - let mut contracts = ActiveNodeContracts::::get(&node_contract.node_id); - - match contracts.iter().position(|id| id == &contract.contract_id) { - Some(index) => { - // if the new contract state is delete, remove the contract id from the map - if contract.is_state_delete() { - contracts.remove(index); - } - } - None => { - // if the contract is not present add it to the active contracts map - if state == &types::ContractState::Created { - contracts.push(contract.contract_id); - } - } - }; - - ActiveNodeContracts::::insert(&node_contract.node_id, &contracts); - - Ok(().into()) - } - - fn remove_active_node_contract(node_id: u32, contract_id: u64) { - let mut contracts = ActiveNodeContracts::::get(&node_id); - - match contracts.iter().position(|id| id == &contract_id) { - Some(index) => { - contracts.remove(index); - } - None => (), - }; - - ActiveNodeContracts::::insert(&node_id, &contracts); - } - - pub fn _reserve_ip( - contract_id: u64, - node_contract: &mut types::NodeContract, - ) -> DispatchResultWithPostInfo { - if node_contract.public_ips == 0 { - return Ok(().into()); - } - let node = pallet_tfgrid::Nodes::::get(node_contract.node_id) - .ok_or(Error::::NodeNotExists)?; - - let mut farm = - pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; - - log::debug!( - "Number of farm ips {:?}, number of ips to reserve: {:?}", - farm.public_ips.len(), - node_contract.public_ips as usize - ); - ensure!( - farm.public_ips.len() >= node_contract.public_ips as usize, - Error::::FarmHasNotEnoughPublicIPs - ); - - let mut ips: BoundedVec> = vec![].try_into().unwrap(); - - for i in 0..farm.public_ips.len() { - if ips.len() == node_contract.public_ips as usize { - break; - } - - // if an ip has contract id 0 it means it's not reserved - // reserve it now - if farm.public_ips[i].contract_id == 0 { - let mut ip = farm.public_ips[i].clone(); - ip.contract_id = contract_id; - farm.public_ips[i] = ip.clone(); - ips.try_push(ip).or_else(|_| { - return Err(DispatchErrorWithPostInfo::from( - Error::::FailedToReserveIP, - )); - })?; - } - } - - // Safeguard check if we actually have the amount of ips we wanted to reserve - ensure!( - ips.len() == node_contract.public_ips as usize, - Error::::FarmHasNotEnoughPublicIPsFree - ); - - node_contract.public_ips_list = ips.try_into().or_else(|_| { - return Err(DispatchErrorWithPostInfo::from( - Error::::FailedToReserveIP, - )); - })?; - - // Update the farm with the reserved ips - pallet_tfgrid::Farms::::insert(farm.id, farm); - - Ok(().into()) - } - - pub fn _free_ip( - contract_id: u64, - node_contract: &mut types::NodeContract, - ) -> DispatchResultWithPostInfo { - let node = pallet_tfgrid::Nodes::::get(node_contract.node_id) - .ok_or(Error::::NodeNotExists)?; - - let mut farm = - pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; - - let mut public_ips: BoundedVec> = - vec![].try_into().unwrap(); - for i in 0..farm.public_ips.len() { - // if an ip has contract id 0 it means it's not reserved - // reserve it now - if farm.public_ips[i].contract_id == contract_id { - let mut ip = farm.public_ips[i].clone(); - ip.contract_id = 0; - farm.public_ips[i] = ip.clone(); - public_ips.try_push(ip).or_else(|_| { - return Err(DispatchErrorWithPostInfo::from(Error::::FailedToFreeIPs)); - })?; - } - } - - pallet_tfgrid::Farms::::insert(farm.id, farm); - - // Emit an event containing the IP's freed for this contract - Self::deposit_event(Event::IPsFreed { - contract_id, - public_ips, - }); - - Ok(().into()) - } - - pub fn get_node_contract( - contract: &types::Contract, - ) -> Result, DispatchErrorWithPostInfo> { - match contract.contract_type.clone() { - types::ContractData::NodeContract(c) => Ok(c), - _ => { - return Err(DispatchErrorWithPostInfo::from( - Error::::InvalidContractType, - )) - } - } - } - - pub fn get_rent_contract( - contract: &types::Contract, - ) -> Result { - match contract.contract_type.clone() { - types::ContractData::RentContract(c) => Ok(c), - _ => { - return Err(DispatchErrorWithPostInfo::from( - Error::::InvalidContractType, - )) - } - } - } - - // Get the usable balance of an account - // This is the balance minus the minimum balance - fn get_usable_balance(account_id: &T::AccountId) -> BalanceOf { - let balance = pallet_balances::pallet::Pallet::::usable_balance(account_id); - let b = balance.saturated_into::(); - BalanceOf::::saturated_from(b) - } - - fn get_locked_balance(account_id: &T::AccountId) -> BalanceOf { - let usable_balance = Self::get_usable_balance(account_id); - let free_balance = ::Currency::free_balance(account_id); - - let locked_balance = free_balance.checked_sub(&usable_balance); - match locked_balance { - Some(balance) => balance, - None => BalanceOf::::zero(), - } - } - - fn get_stash_balance(twin_id: u32) -> BalanceOf { - let account_id = pallet_tfgrid::TwinBoundedAccountID::::get(twin_id); - match account_id { - Some(account) => Self::get_usable_balance(&account), - None => BalanceOf::::zero(), - } - } - - pub fn _create_solution_provider( - description: Vec, - link: Vec, - providers: Vec>, - ) -> DispatchResultWithPostInfo { - let total_take: u8 = providers.iter().map(|provider| provider.take).sum(); - ensure!(total_take <= 50, Error::::InvalidProviderConfiguration); - - let mut id = SolutionProviderID::::get(); - id = id + 1; - - let solution_provider = types::SolutionProvider { - solution_provider_id: id, - providers, - description, - link, - approved: false, - }; - - SolutionProviderID::::put(id); - SolutionProviders::::insert(id, &solution_provider); - - Self::deposit_event(Event::SolutionProviderCreated(solution_provider)); - - Ok(().into()) - } - - pub fn _approve_solution_provider( - solution_provider_id: u64, - approve: bool, - ) -> DispatchResultWithPostInfo { - ensure!( - SolutionProviders::::contains_key(solution_provider_id), - Error::::NoSuchSolutionProvider - ); - - if let Some(mut solution_provider) = SolutionProviders::::get(solution_provider_id) { - solution_provider.approved = approve; - SolutionProviders::::insert(solution_provider_id, &solution_provider); - Self::deposit_event(Event::SolutionProviderApproved( - solution_provider_id, - approve, - )); - } - - Ok(().into()) - } - - pub fn validate_solution_provider( - solution_provider_id: Option, - ) -> DispatchResultWithPostInfo { - if let Some(provider_id) = solution_provider_id { - ensure!( - SolutionProviders::::contains_key(provider_id), - Error::::NoSuchSolutionProvider - ); - - if let Some(solution_provider) = SolutionProviders::::get(provider_id) { - ensure!( - solution_provider.approved, - Error::::SolutionProviderNotApproved - ); - return Ok(().into()); - } - } - Ok(().into()) - } - - // Billing index is contract id % (mod) Billing Frequency - // So index belongs to [0; billing_frequency - 1] range - pub fn get_contract_billing_loop_index(contract_id: u64) -> u64 { - contract_id % BillingFrequency::::get() - } - - // Billing index is block number % (mod) Billing Frequency - // So index belongs to [0; billing_frequency - 1] range - pub fn get_current_billing_loop_index() -> u64 { - let current_block = >::block_number().saturated_into::(); - current_block % BillingFrequency::::get() - } - - pub fn _service_contract_create( - caller: T::AccountId, - service: T::AccountId, - consumer: T::AccountId, - ) -> DispatchResultWithPostInfo { - let caller_twin_id = - pallet_tfgrid::TwinIdByAccountID::::get(&caller).ok_or(Error::::TwinNotExists)?; - - let service_twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&service) - .ok_or(Error::::TwinNotExists)?; - - let consumer_twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&consumer) - .ok_or(Error::::TwinNotExists)?; - - // Only service or consumer can create contract - ensure!( - caller_twin_id == service_twin_id || caller_twin_id == consumer_twin_id, - Error::::TwinNotAuthorized, - ); - - // Service twin and consumer twin can not be the same - ensure!( - service_twin_id != consumer_twin_id, - Error::::ServiceContractCreationNotAllowed, - ); - - // Get the service contract ID map and increment - let mut id = ServiceContractID::::get(); - id = id + 1; - - // Create service contract - let service_contract = types::ServiceContract { - service_contract_id: id, - service_twin_id, - consumer_twin_id, - base_fee: 0, - variable_fee: 0, - metadata: vec![].try_into().unwrap(), - accepted_by_service: false, - accepted_by_consumer: false, - last_bill: 0, - state: types::ServiceContractState::Created, - }; - - // Insert into service contract map - ServiceContracts::::insert(id, &service_contract); - - // Update Contract ID - ServiceContractID::::put(id); - - // Trigger event for service contract creation - Self::deposit_event(Event::ServiceContractCreated(service_contract)); - - Ok(().into()) - } - - pub fn _service_contract_set_metadata( - account_id: T::AccountId, - service_contract_id: u64, - metadata: Vec, - ) -> DispatchResultWithPostInfo { - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - - let mut service_contract = ServiceContracts::::get(service_contract_id) - .ok_or(Error::::ServiceContractNotExists)?; - - // Only service or consumer can set metadata - ensure!( - twin_id == service_contract.service_twin_id - || twin_id == service_contract.consumer_twin_id, - Error::::TwinNotAuthorized, - ); - - // Only allow to modify metadata if contract still not approved by both parties - ensure!( - !matches!( - service_contract.state, - types::ServiceContractState::ApprovedByBoth - ), - Error::::ServiceContractModificationNotAllowed, - ); - - service_contract.metadata = BoundedVec::try_from(metadata) - .map_err(|_| Error::::ServiceContractMetadataTooLong)?; - - // If base_fee is set and non-zero (mandatory) - if service_contract.base_fee != 0 { - service_contract.state = types::ServiceContractState::AgreementReady; - } - - // Update service contract in map after modification - ServiceContracts::::insert(service_contract_id, service_contract.clone()); - - // Trigger event for service contract metadata setting - Self::deposit_event(Event::ServiceContractMetadataSet(service_contract)); - - Ok(().into()) - } - - pub fn _service_contract_set_fees( - account_id: T::AccountId, - service_contract_id: u64, - base_fee: u64, - variable_fee: u64, - ) -> DispatchResultWithPostInfo { - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - - let mut service_contract = ServiceContracts::::get(service_contract_id) - .ok_or(Error::::ServiceContractNotExists)?; - - // Only service can set fees - ensure!( - twin_id == service_contract.service_twin_id, - Error::::TwinNotAuthorized, - ); - - // Only allow to modify fees if contract still not approved by both parties - ensure!( - !matches!( - service_contract.state, - types::ServiceContractState::ApprovedByBoth - ), - Error::::ServiceContractModificationNotAllowed, - ); - - service_contract.base_fee = base_fee; - service_contract.variable_fee = variable_fee; - - // If metadata is filled and not empty (mandatory) - if !service_contract.metadata.is_empty() { - service_contract.state = types::ServiceContractState::AgreementReady; - } - - // Update service contract in map after modification - ServiceContracts::::insert(service_contract_id, service_contract.clone()); - - // Trigger event for service contract fees setting - Self::deposit_event(Event::ServiceContractFeesSet(service_contract)); - - Ok(().into()) - } - - pub fn _service_contract_approve( - account_id: T::AccountId, - service_contract_id: u64, - ) -> DispatchResultWithPostInfo { - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - - let mut service_contract = ServiceContracts::::get(service_contract_id) - .ok_or(Error::::ServiceContractNotExists)?; - - // Allow to approve contract only if agreement is ready - ensure!( - matches!( - service_contract.state, - types::ServiceContractState::AgreementReady - ), - Error::::ServiceContractApprovalNotAllowed, - ); - - // Only service or consumer can accept agreement - if twin_id == service_contract.service_twin_id { - service_contract.accepted_by_service = true; - } else if twin_id == service_contract.consumer_twin_id { - service_contract.accepted_by_consumer = true - } else { - return Err(DispatchErrorWithPostInfo::from( - Error::::TwinNotAuthorized, - )); - } - - // If both parties (service and consumer) accept then contract is approved and can be billed - if service_contract.accepted_by_service && service_contract.accepted_by_consumer { - // Change contract state to approved and emit event - service_contract.state = types::ServiceContractState::ApprovedByBoth; - - // Initialize billing time - let now = >::get().saturated_into::() / 1000; - service_contract.last_bill = now; - } - - // Update service contract in map after modification - ServiceContracts::::insert(service_contract_id, service_contract.clone()); - - // Trigger event for service contract approval - Self::deposit_event(Event::ServiceContractApproved(service_contract)); - - Ok(().into()) - } - - pub fn _service_contract_reject( - account_id: T::AccountId, - service_contract_id: u64, - ) -> DispatchResultWithPostInfo { - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - - let service_contract = ServiceContracts::::get(service_contract_id) - .ok_or(Error::::ServiceContractNotExists)?; - - // Only service or consumer can reject agreement - ensure!( - twin_id == service_contract.service_twin_id - || twin_id == service_contract.consumer_twin_id, - Error::::TwinNotAuthorized, - ); - - // Allow to reject contract only if agreement is ready - ensure!( - matches!( - service_contract.state, - types::ServiceContractState::AgreementReady - ), - Error::::ServiceContractRejectionNotAllowed, - ); - - // If one party (service or consumer) rejects agreement - // then contract is canceled and removed from service contract map - Self::_service_contract_cancel(twin_id, service_contract_id, types::Cause::CanceledByUser)?; - - Ok(().into()) - } - - pub fn _service_contract_cancel( - twin_id: u32, - service_contract_id: u64, - cause: types::Cause, - ) -> DispatchResultWithPostInfo { - let service_contract = ServiceContracts::::get(service_contract_id) - .ok_or(Error::::ServiceContractNotExists)?; - - // Only service or consumer can cancel contract - ensure!( - twin_id == service_contract.service_twin_id - || twin_id == service_contract.consumer_twin_id, - Error::::TwinNotAuthorized, - ); - - // Remove contract from service contract map - // Can be done at any state of contract - // so no need to handle state validation - ServiceContracts::::remove(service_contract_id); - - // Trigger event for service contract cancelation - Self::deposit_event(Event::ServiceContractCanceled { - service_contract_id, - cause, - }); - - log::debug!( - "successfully removed service contract with id {:?}", - service_contract_id, - ); - - Ok(().into()) - } - - #[transactional] - pub fn _service_contract_bill( - account_id: T::AccountId, - service_contract_id: u64, - variable_amount: u64, - metadata: Vec, - ) -> DispatchResultWithPostInfo { - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - - let mut service_contract = ServiceContracts::::get(service_contract_id) - .ok_or(Error::::ServiceContractNotExists)?; - - // Only service can bill consumer for service contract - ensure!( - twin_id == service_contract.service_twin_id, - Error::::TwinNotAuthorized, - ); - - // Allow to bill contract only if approved by both - ensure!( - matches!( - service_contract.state, - types::ServiceContractState::ApprovedByBoth - ), - Error::::ServiceContractBillingNotApprovedByBoth, - ); - - // Get elapsed time (in seconds) to bill for service - let now = >::get().saturated_into::() / 1000; - let elapsed_seconds_since_last_bill = now - service_contract.last_bill; - - // Billing time (window) is max 1h by design - // So extra time will not be billed - // It is the service responsability to bill on right frequency - let window = elapsed_seconds_since_last_bill.min(T::BillingReferencePeriod::get()); - - // Billing variable amount is bounded by contract variable fee - ensure!( - variable_amount - <= ((U64F64::from_num(window) - / U64F64::from_num(T::BillingReferencePeriod::get())) - * U64F64::from_num(service_contract.variable_fee)) - .round() - .to_num::(), - Error::::ServiceContractBillingVariableAmountTooHigh, - ); - - let bill_metadata = BoundedVec::try_from(metadata) - .map_err(|_| Error::::ServiceContractBillMetadataTooLong)?; - - // Create service contract bill - let service_contract_bill = types::ServiceContractBill { - variable_amount, - window, - metadata: bill_metadata, - }; - - // Make consumer pay for service contract bill - let amount = - Self::_service_contract_pay_bill(service_contract_id, service_contract_bill.clone())?; - - // Update contract in list after modification - service_contract.last_bill = now; - ServiceContracts::::insert(service_contract_id, service_contract.clone()); - - // Trigger event for service contract billing - Self::deposit_event(Event::ServiceContractBilled { - service_contract, - bill: service_contract_bill, - amount, - }); - - Ok(().into()) - } - - // Pay a service contract bill - // Calculates how much TFT is due by the consumer and pay the amount to the service - fn _service_contract_pay_bill( - service_contract_id: u64, - bill: types::ServiceContractBill, - ) -> Result, DispatchErrorWithPostInfo> { - let service_contract = ServiceContracts::::get(service_contract_id) - .ok_or(Error::::ServiceContractNotExists)?; - let amount = service_contract.calculate_bill_cost_tft::(bill.clone())?; - - let service_twin_id = service_contract.service_twin_id; - let service_twin = - pallet_tfgrid::Twins::::get(service_twin_id).ok_or(Error::::TwinNotExists)?; - - let consumer_twin = pallet_tfgrid::Twins::::get(service_contract.consumer_twin_id) - .ok_or(Error::::TwinNotExists)?; - - let usable_balance = Self::get_usable_balance(&consumer_twin.account_id); - - // If consumer is out of funds then contract is canceled - // by service and removed from service contract map - if usable_balance < amount { - Self::_service_contract_cancel( - service_twin_id, - service_contract_id, - types::Cause::OutOfFunds, - )?; - return Err(DispatchErrorWithPostInfo::from( - Error::::ServiceContractNotEnoughFundsToPayBill, - )); - } - - // Transfer amount due from consumer account to service account - ::Currency::transfer( - &consumer_twin.account_id, - &service_twin.account_id, - amount, - ExistenceRequirement::KeepAlive, - )?; - - log::debug!( - "bill successfully payed by consumer for service contract with id {:?}", - service_contract_id, - ); - - Ok(amount) - } - - pub fn _change_billing_frequency(frequency: u64) -> DispatchResultWithPostInfo { - let billing_frequency = BillingFrequency::::get(); - ensure!( - frequency > billing_frequency, - Error::::CanOnlyIncreaseFrequency - ); - - BillingFrequency::::put(frequency); - - Self::deposit_event(Event::BillingFrequencyChanged(frequency)); - - Ok(().into()) - } - - pub fn _attach_solution_provider_id( - account_id: T::AccountId, - contract_id: u64, - solution_provider_id: u64, - ) -> DispatchResultWithPostInfo { - let solution_provider = SolutionProviders::::get(solution_provider_id) - .ok_or(Error::::NoSuchSolutionProvider)?; - ensure!( - solution_provider.approved, - Error::::SolutionProviderNotApproved - ); - - let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - - ensure!( - contract.twin_id == twin_id, - Error::::UnauthorizedToChangeSolutionProviderId - ); - - match contract.solution_provider_id { - Some(_) => { - return Err(DispatchErrorWithPostInfo::from( - Error::::UnauthorizedToChangeSolutionProviderId, - )) - } - None => { - contract.solution_provider_id = Some(solution_provider_id); - Contracts::::insert(contract_id, &contract); - Self::deposit_event(Event::ContractUpdated(contract)); - } - }; - - Ok(().into()) - } - - pub fn _set_dedicated_node_extra_fee( - account_id: T::AccountId, - node_id: u32, - extra_fee: u64, - ) -> DispatchResultWithPostInfo { - // Make sure only the farmer that owns this node can set the extra fee - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; - let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; - ensure!( - twin_id == farm.twin_id, - Error::::UnauthorizedToSetExtraFee - ); - - // Make sure there is no active node or rent contract on this node - ensure!( - ActiveRentContractForNode::::get(node_id).is_none() - && ActiveNodeContracts::::get(&node_id).is_empty(), - Error::::NodeHasActiveContracts - ); - - // Set fee in mUSD - DedicatedNodesExtraFee::::insert(node_id, extra_fee); - Self::deposit_event(Event::NodeExtraFeeSet { node_id, extra_fee }); - - Ok(().into()) - } -} - -impl ChangeNode, InterfaceOf, SerialNumberOf> for Pallet { - fn node_changed(_node: Option<&TfgridNode>, _new_node: &TfgridNode) {} - - fn node_deleted(node: &TfgridNode) { - // Clean up all active contracts - let active_node_contracts = ActiveNodeContracts::::get(node.id); - for node_contract_id in active_node_contracts { - if let Some(mut contract) = Contracts::::get(node_contract_id) { - // Bill contract - let _ = Self::_update_contract_state( - &mut contract, - &types::ContractState::Deleted(types::Cause::CanceledByUser), - ); - let _ = Self::bill_contract(node_contract_id); - } - } - - // First clean up rent contract if it exists - if let Some(rc_id) = ActiveRentContractForNode::::get(node.id) { - if let Some(mut contract) = Contracts::::get(rc_id) { - // Bill contract - let _ = Self::_update_contract_state( - &mut contract, - &types::ContractState::Deleted(types::Cause::CanceledByUser), - ); - let _ = Self::bill_contract(contract.contract_id); - } - } - } -} - -impl PublicIpModifier for Pallet { - fn ip_removed(ip: &PublicIP) { - if let Some(mut contract) = Contracts::::get(ip.contract_id) { - match contract.contract_type { - types::ContractData::NodeContract(mut node_contract) => { - if node_contract.public_ips > 0 { - if let Err(e) = Self::_free_ip(ip.contract_id, &mut node_contract) { - log::error!("error while freeing ips: {:?}", e); - } - } - contract.contract_type = types::ContractData::NodeContract(node_contract); - - Contracts::::insert(ip.contract_id, &contract); - } - _ => {} - } - } - } -} - -impl Pallet { - // Validates if the given signer is the next block author based on the validators in session - // This can be used if an extrinsic should be refunded by the author in the same block - // It also requires that the keytype inserted for the offchain workers is the validator key - fn is_next_block_author( - signer: &Signer::AuthorityId>, - ) -> Result<(), Error> { - let author = >::author(); - let validators = >::validators(); - - // Sign some arbitrary data in order to get the AccountId, maybe there is another way to do this? - let signed_message = signer.sign_message(&[0]); - if let Some(signed_message_data) = signed_message { - if let Some(block_author) = author { - let validator = - ::ValidatorIdOf::convert(block_author.clone()) - .ok_or(Error::::IsNotAnAuthority)?; - - let validator_count = validators.len(); - let author_index = (validators.iter().position(|a| a == &validator).unwrap_or(0) - + 1) - % validator_count; - - let signer_validator_account = - ::ValidatorIdOf::convert( - signed_message_data.0.id.clone(), - ) - .ok_or(Error::::IsNotAnAuthority)?; - - if signer_validator_account != validators[author_index] { - return Err(Error::::WrongAuthority); - } - } - } - - Ok(().into()) } } diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/v10.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/v10.rs index 0dd663881..b067254f0 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/migrations/v10.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/v10.rs @@ -1,8 +1,9 @@ use crate::*; use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; use log::info; +use sp_core::Get; use sp_runtime::Saturating; -use sp_std::marker::PhantomData; +use sp_std::{marker::PhantomData, vec}; #[cfg(feature = "try-runtime")] use sp_std::vec::Vec; diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs index c72b33bd8..9d7b5e9e2 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs @@ -4,10 +4,12 @@ use frame_support::{ Blake2_128Concat, }; use log::{debug, info}; +use sp_core::Get; +use sp_runtime::traits::Zero; use sp_std::marker::PhantomData; #[cfg(feature = "try-runtime")] -use sp_std::vec::Vec; +use sp_std::{vec, vec::Vec}; // Storage alias from ContractLock v11 #[storage_alias] diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/v6.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/v6.rs index 512dd9e18..7d6df417f 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/migrations/v6.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/v6.rs @@ -1,12 +1,11 @@ use crate::*; use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; use log::{debug, info}; -use sp_std::marker::PhantomData; +use sp_core::Get; +use sp_std::{marker::PhantomData, vec::Vec}; #[cfg(feature = "try-runtime")] use parity_scale_codec::{Decode, Encode}; -#[cfg(feature = "try-runtime")] -use sp_std::vec::Vec; pub struct ContractMigrationV5(PhantomData); diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/v8.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/v8.rs index 5dadcc899..9c2214474 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/migrations/v8.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/v8.rs @@ -1,11 +1,15 @@ use crate::*; -use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; +use frame_support::{ + traits::{Currency, LockableCurrency, OnRuntimeUpgrade, WithdrawReasons}, + weights::Weight, +}; use log::{debug, info}; -use sp_std::collections::btree_map::BTreeMap; -use sp_std::marker::PhantomData; +use sp_core::Get; +use sp_runtime::traits::{CheckedSub, SaturatedConversion}; +use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData}; #[cfg(feature = "try-runtime")] -use sp_std::vec::Vec; +use sp_std::{vec, vec::Vec}; pub struct FixTwinLockedBalances(PhantomData); diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/v9.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/v9.rs index 9f0d2b285..d420af067 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/migrations/v9.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/v9.rs @@ -1,10 +1,10 @@ -use crate::pallet_tfgrid; use crate::*; use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; use log::{info, debug}; use sp_runtime::Saturating; -use sp_std::marker::PhantomData; use scale_info::prelude::string::String; +use sp_core::Get; +use sp_std::{marker::PhantomData, vec, vec::Vec}; pub struct CleanStorageState(PhantomData); @@ -136,7 +136,7 @@ pub fn check_contracts() { ); } -fn check_node_contract(node_id: u32, contract_id: u64, deployment_hash: HexHash) { +fn check_node_contract(node_id: u32, contract_id: u64, deployment_hash: types::HexHash) { if pallet_tfgrid::Nodes::::get(node_id).is_some() { // ActiveNodeContracts let active_node_contracts = ActiveNodeContracts::::get(node_id); @@ -180,7 +180,7 @@ fn check_node_contract(node_id: u32, contract_id: u64, deployment_has ); } - if deployment_hash == HexHash::default() { + if deployment_hash == types::HexHash::default() { debug!( " ⚠️ Node Contract (id: {}) on node {}: deployment hash is default ({:?})", contract_id, node_id, String::from_utf8_lossy(&deployment_hash) @@ -592,7 +592,7 @@ pub fn clean_contracts() -> frame_support::weights::Weight { // ContractLock if !ContractLock::::contains_key(contract_id) { - let now = >::get().saturated_into::() / 1000; + let now = Pallet::::get_current_timestamp_in_secs(); r.saturating_inc(); let mut contract_lock = types::ContractLock::default(); contract_lock.lock_updated = now; @@ -610,8 +610,8 @@ pub fn clean_contracts() -> frame_support::weights::Weight { T::DbWeight::get().reads_writes(r.saturating_add(2), w) } -fn clean_node_contract(node_id: u32, contract_id: u64, deployment_hash: HexHash, r: &mut u64, w: &mut u64) { - if deployment_hash == HexHash::default() { +fn clean_node_contract(node_id: u32, contract_id: u64, deployment_hash: types::HexHash, r: &mut u64, w: &mut u64) { + if deployment_hash == types::HexHash::default() { Contracts::::remove(contract_id); (*w).saturating_inc(); } diff --git a/substrate-node/pallets/pallet-smart-contract/src/mock.rs b/substrate-node/pallets/pallet-smart-contract/src/mock.rs index 3e421a873..c786d6487 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/mock.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/mock.rs @@ -1,9 +1,9 @@ #![cfg(test)] use super::*; -use crate::name_contract::NameContractName; -use crate::{self as pallet_smart_contract}; +use crate::{self as pallet_smart_contract, crypto::KEY_TYPE, grid_contract::NameContractName}; use frame_support::{ construct_runtime, + dispatch::DispatchErrorWithPostInfo, dispatch::PostDispatchInfo, parameter_types, traits::{ConstU32, GenesisBuild}, @@ -22,8 +22,7 @@ use pallet_tfgrid::{ use parity_scale_codec::{alloc::sync::Arc, Decode, Encode}; use parking_lot::RwLock; use sp_core::{ - crypto::key_types::DUMMY, - crypto::Ss58Codec, + crypto::{key_types::DUMMY, KeyTypeId, Ss58Codec}, offchain::{ testing::{self}, OffchainDbExt, OffchainWorkerExt, TransactionPoolExt, @@ -40,12 +39,15 @@ use sp_runtime::{ }, AccountId32, MultiSignature, }; -use sp_std::convert::{TryFrom, TryInto}; -use sp_std::marker::PhantomData; +use sp_std::{ + convert::{TryFrom, TryInto}, + marker::PhantomData, +}; use std::{cell::RefCell, panic, thread}; use tfchain_support::{ constants::time::{MINUTES, SECS_PER_HOUR}, - traits::ChangeNode, + traits::{ChangeNode, PublicIpModifier}, + types::PublicIP, }; impl_opaque_keys! { diff --git a/substrate-node/pallets/pallet-smart-contract/src/name_contract.rs b/substrate-node/pallets/pallet-smart-contract/src/name_contract.rs deleted file mode 100644 index 8cab372ae..000000000 --- a/substrate-node/pallets/pallet-smart-contract/src/name_contract.rs +++ /dev/null @@ -1,69 +0,0 @@ -use sp_std::{marker::PhantomData, vec::Vec}; - -use frame_support::{ensure, BoundedVec, RuntimeDebugNoBound}; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; -use scale_info::TypeInfo; -use sp_std::convert::TryFrom; - -use crate::{Config, Error}; - -/// A Name Contract Name. -#[derive(Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] -#[scale_info(skip_type_params(T))] -#[codec(mel_bound())] -pub struct NameContractName( - pub(crate) BoundedVec, - PhantomData<(T, T::MaxNameContractNameLength)>, -); - -pub const MIN_NAME_LENGTH: u32 = 3; - -impl TryFrom> for NameContractName { - type Error = Error; - - /// Fallible initialization from a provided byte vector if it is below the - /// minimum or exceeds the maximum allowed length or contains invalid ASCII - /// characters. - fn try_from(value: Vec) -> Result { - ensure!( - value.len() >= MIN_NAME_LENGTH as usize, - Self::Error::NameContractNameTooShort - ); - let bounded_vec: BoundedVec = - BoundedVec::try_from(value).map_err(|_| Self::Error::NameContractNameTooLong)?; - ensure!( - is_valid_name_contract_name(&bounded_vec), - Self::Error::NameNotValid - ); - Ok(Self(bounded_vec, PhantomData)) - } -} - -/// Verify that a given slice can be used as a name contract name. -fn is_valid_name_contract_name(input: &[u8]) -> bool { - input - .iter() - .all(|c| matches!(c, b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_')) -} - -impl From> for Vec { - fn from(value: NameContractName) -> Self { - value.0.to_vec() - } -} - -// FIXME: did not find a way to automatically implement this. -impl PartialEq for NameContractName { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for NameContractName {} - -// FIXME: did not find a way to automatically implement this. -impl Clone for NameContractName { - fn clone(&self) -> Self { - Self(self.0.clone(), self.1) - } -} diff --git a/substrate-node/pallets/pallet-smart-contract/src/service_contract.rs b/substrate-node/pallets/pallet-smart-contract/src/service_contract.rs new file mode 100644 index 000000000..18d98f91f --- /dev/null +++ b/substrate-node/pallets/pallet-smart-contract/src/service_contract.rs @@ -0,0 +1,393 @@ +use crate::*; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo}, + ensure, + traits::{Currency, ExistenceRequirement}, + transactional, BoundedVec, +}; +use sp_core::Get; +use sp_std::{vec, vec::Vec}; +use substrate_fixed::types::U64F64; + +impl Pallet { + pub fn _service_contract_create( + caller: T::AccountId, + service: T::AccountId, + consumer: T::AccountId, + ) -> DispatchResultWithPostInfo { + let caller_twin_id = + pallet_tfgrid::TwinIdByAccountID::::get(&caller).ok_or(Error::::TwinNotExists)?; + + let service_twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&service) + .ok_or(Error::::TwinNotExists)?; + + let consumer_twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&consumer) + .ok_or(Error::::TwinNotExists)?; + + // Only service or consumer can create contract + ensure!( + caller_twin_id == service_twin_id || caller_twin_id == consumer_twin_id, + Error::::TwinNotAuthorized, + ); + + // Service twin and consumer twin can not be the same + ensure!( + service_twin_id != consumer_twin_id, + Error::::ServiceContractCreationNotAllowed, + ); + + // Get the service contract ID map and increment + let mut id = ServiceContractID::::get(); + id = id + 1; + + // Create service contract + let service_contract = types::ServiceContract { + service_contract_id: id, + service_twin_id, + consumer_twin_id, + base_fee: 0, + variable_fee: 0, + metadata: vec![].try_into().unwrap(), + accepted_by_service: false, + accepted_by_consumer: false, + last_bill: 0, + state: types::ServiceContractState::Created, + }; + + // Insert into service contract map + ServiceContracts::::insert(id, &service_contract); + + // Update Contract ID + ServiceContractID::::put(id); + + // Trigger event for service contract creation + Self::deposit_event(Event::ServiceContractCreated(service_contract)); + + Ok(().into()) + } + + pub fn _service_contract_set_metadata( + account_id: T::AccountId, + service_contract_id: u64, + metadata: Vec, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service or consumer can set metadata + ensure!( + twin_id == service_contract.service_twin_id + || twin_id == service_contract.consumer_twin_id, + Error::::TwinNotAuthorized, + ); + + // Only allow to modify metadata if contract still not approved by both parties + ensure!( + !matches!( + service_contract.state, + types::ServiceContractState::ApprovedByBoth + ), + Error::::ServiceContractModificationNotAllowed, + ); + + service_contract.metadata = BoundedVec::try_from(metadata) + .map_err(|_| Error::::ServiceContractMetadataTooLong)?; + + // If base_fee is set and non-zero (mandatory) + if service_contract.base_fee != 0 { + service_contract.state = types::ServiceContractState::AgreementReady; + } + + // Update service contract in map after modification + ServiceContracts::::insert(service_contract_id, service_contract.clone()); + + // Trigger event for service contract metadata setting + Self::deposit_event(Event::ServiceContractMetadataSet(service_contract)); + + Ok(().into()) + } + + pub fn _service_contract_set_fees( + account_id: T::AccountId, + service_contract_id: u64, + base_fee: u64, + variable_fee: u64, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service can set fees + ensure!( + twin_id == service_contract.service_twin_id, + Error::::TwinNotAuthorized, + ); + + // Only allow to modify fees if contract still not approved by both parties + ensure!( + !matches!( + service_contract.state, + types::ServiceContractState::ApprovedByBoth + ), + Error::::ServiceContractModificationNotAllowed, + ); + + service_contract.base_fee = base_fee; + service_contract.variable_fee = variable_fee; + + // If metadata is filled and not empty (mandatory) + if !service_contract.metadata.is_empty() { + service_contract.state = types::ServiceContractState::AgreementReady; + } + + // Update service contract in map after modification + ServiceContracts::::insert(service_contract_id, service_contract.clone()); + + // Trigger event for service contract fees setting + Self::deposit_event(Event::ServiceContractFeesSet(service_contract)); + + Ok(().into()) + } + + pub fn _service_contract_approve( + account_id: T::AccountId, + service_contract_id: u64, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Allow to approve contract only if agreement is ready + ensure!( + matches!( + service_contract.state, + types::ServiceContractState::AgreementReady + ), + Error::::ServiceContractApprovalNotAllowed, + ); + + // Only service or consumer can accept agreement + if twin_id == service_contract.service_twin_id { + service_contract.accepted_by_service = true; + } else if twin_id == service_contract.consumer_twin_id { + service_contract.accepted_by_consumer = true + } else { + return Err(DispatchErrorWithPostInfo::from( + Error::::TwinNotAuthorized, + )); + } + + // If both parties (service and consumer) accept then contract is approved and can be billed + if service_contract.accepted_by_service && service_contract.accepted_by_consumer { + // Change contract state to approved and emit event + service_contract.state = types::ServiceContractState::ApprovedByBoth; + + // Initialize billing time + let now = Self::get_current_timestamp_in_secs(); + service_contract.last_bill = now; + } + + // Update service contract in map after modification + ServiceContracts::::insert(service_contract_id, service_contract.clone()); + + // Trigger event for service contract approval + Self::deposit_event(Event::ServiceContractApproved(service_contract)); + + Ok(().into()) + } + + pub fn _service_contract_reject( + account_id: T::AccountId, + service_contract_id: u64, + ) -> DispatchResultWithPostInfo { + let service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Allow to reject contract only if agreement is ready + ensure!( + matches!( + service_contract.state, + types::ServiceContractState::AgreementReady + ), + Error::::ServiceContractRejectionNotAllowed, + ); + + // If one party (service or consumer) rejects agreement + // then contract is canceled and removed from service contract map + Self::_service_contract_cancel( + account_id, + service_contract_id, + types::Cause::CanceledByUser, + )?; + + Ok(().into()) + } + + pub fn _service_contract_cancel( + account_id: T::AccountId, + service_contract_id: u64, + cause: types::Cause, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service or consumer can cancel contract + ensure!( + twin_id == service_contract.service_twin_id + || twin_id == service_contract.consumer_twin_id, + Error::::TwinNotAuthorized, + ); + + // Remove contract from service contract map + // Can be done at any state of contract + // so no need to handle state validation + ServiceContracts::::remove(service_contract_id); + + // Trigger event for service contract cancelation + Self::deposit_event(Event::ServiceContractCanceled { + service_contract_id, + cause, + }); + + log::debug!( + "successfully removed service contract with id {:?}", + service_contract_id, + ); + + Ok(().into()) + } + + #[transactional] + pub fn _service_contract_bill( + account_id: T::AccountId, + service_contract_id: u64, + variable_amount: u64, + metadata: Vec, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service can bill consumer for service contract + ensure!( + twin_id == service_contract.service_twin_id, + Error::::TwinNotAuthorized, + ); + + // Allow to bill contract only if approved by both + ensure!( + matches!( + service_contract.state, + types::ServiceContractState::ApprovedByBoth + ), + Error::::ServiceContractBillingNotApprovedByBoth, + ); + + // Get elapsed time (in seconds) to bill for service + let now = Self::get_current_timestamp_in_secs(); + let elapsed_seconds_since_last_bill = now - service_contract.last_bill; + + // Billing time (window) is max 1h by design + // So extra time will not be billed + // It is the service responsability to bill on right frequency + let window = elapsed_seconds_since_last_bill.min(T::BillingReferencePeriod::get()); + + // Billing variable amount is bounded by contract variable fee + ensure!( + variable_amount + <= ((U64F64::from_num(window) + / U64F64::from_num(T::BillingReferencePeriod::get())) + * U64F64::from_num(service_contract.variable_fee)) + .round() + .to_num::(), + Error::::ServiceContractBillingVariableAmountTooHigh, + ); + + let bill_metadata = BoundedVec::try_from(metadata) + .map_err(|_| Error::::ServiceContractBillMetadataTooLong)?; + + // Create service contract bill + let service_contract_bill = types::ServiceContractBill { + variable_amount, + window, + metadata: bill_metadata, + }; + + // Make consumer pay for service contract bill + let amount = + Self::_service_contract_pay_bill(service_contract_id, service_contract_bill.clone())?; + + // Update contract in list after modification + service_contract.last_bill = now; + ServiceContracts::::insert(service_contract_id, service_contract.clone()); + + // Trigger event for service contract billing + Self::deposit_event(Event::ServiceContractBilled { + service_contract, + bill: service_contract_bill, + amount, + }); + + Ok(().into()) + } + + // Pay a service contract bill + // Calculates how much TFT is due by the consumer and pay the amount to the service + fn _service_contract_pay_bill( + service_contract_id: u64, + bill: types::ServiceContractBill, + ) -> Result, DispatchErrorWithPostInfo> { + let service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + let amount = service_contract.calculate_bill_cost_tft::(bill.clone())?; + + let service_twin_id = service_contract.service_twin_id; + let service_twin = + pallet_tfgrid::Twins::::get(service_twin_id).ok_or(Error::::TwinNotExists)?; + + let consumer_twin = pallet_tfgrid::Twins::::get(service_contract.consumer_twin_id) + .ok_or(Error::::TwinNotExists)?; + + let usable_balance = Self::get_usable_balance(&consumer_twin.account_id); + + // If consumer is out of funds then contract is canceled + // by service and removed from service contract map + if usable_balance < amount { + Self::_service_contract_cancel( + service_twin.account_id, + service_contract_id, + types::Cause::OutOfFunds, + )?; + return Err(DispatchErrorWithPostInfo::from( + Error::::ServiceContractNotEnoughFundsToPayBill, + )); + } + + // Transfer amount due from consumer account to service account + ::Currency::transfer( + &consumer_twin.account_id, + &service_twin.account_id, + amount, + ExistenceRequirement::KeepAlive, + )?; + + log::debug!( + "bill successfully payed by consumer for service contract with id {:?}", + service_contract_id, + ); + + Ok(amount) + } +} diff --git a/substrate-node/pallets/pallet-smart-contract/src/solution_provider.rs b/substrate-node/pallets/pallet-smart-contract/src/solution_provider.rs new file mode 100644 index 000000000..0a80d104c --- /dev/null +++ b/substrate-node/pallets/pallet-smart-contract/src/solution_provider.rs @@ -0,0 +1,115 @@ +use crate::*; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo}, + ensure, +}; +use sp_std::vec::Vec; + +impl Pallet { + pub fn _create_solution_provider( + description: Vec, + link: Vec, + providers: Vec>, + ) -> DispatchResultWithPostInfo { + let total_take: u8 = providers.iter().map(|provider| provider.take).sum(); + ensure!(total_take <= 50, Error::::InvalidProviderConfiguration); + + let mut id = SolutionProviderID::::get(); + id = id + 1; + + let solution_provider = types::SolutionProvider { + solution_provider_id: id, + providers, + description, + link, + approved: false, + }; + + SolutionProviderID::::put(id); + SolutionProviders::::insert(id, &solution_provider); + + Self::deposit_event(Event::SolutionProviderCreated(solution_provider)); + + Ok(().into()) + } + + pub fn _approve_solution_provider( + solution_provider_id: u64, + approve: bool, + ) -> DispatchResultWithPostInfo { + ensure!( + SolutionProviders::::contains_key(solution_provider_id), + Error::::NoSuchSolutionProvider + ); + + if let Some(mut solution_provider) = SolutionProviders::::get(solution_provider_id) { + solution_provider.approved = approve; + SolutionProviders::::insert(solution_provider_id, &solution_provider); + + Self::deposit_event(Event::SolutionProviderApproved( + solution_provider_id, + approve, + )); + } + + Ok(().into()) + } + + pub fn _attach_solution_provider_id( + account_id: T::AccountId, + contract_id: u64, + solution_provider_id: u64, + ) -> DispatchResultWithPostInfo { + let solution_provider = SolutionProviders::::get(solution_provider_id) + .ok_or(Error::::NoSuchSolutionProvider)?; + ensure!( + solution_provider.approved, + Error::::SolutionProviderNotApproved + ); + + let mut contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; + + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + ensure!( + contract.twin_id == twin_id, + Error::::UnauthorizedToChangeSolutionProviderId + ); + + match contract.solution_provider_id { + Some(_) => { + return Err(DispatchErrorWithPostInfo::from( + Error::::UnauthorizedToChangeSolutionProviderId, + )) + } + None => { + contract.solution_provider_id = Some(solution_provider_id); + Contracts::::insert(contract_id, &contract); + Self::deposit_event(Event::ContractUpdated(contract)); + } + }; + + Ok(().into()) + } + + pub fn validate_solution_provider( + solution_provider_id: Option, + ) -> DispatchResultWithPostInfo { + if let Some(provider_id) = solution_provider_id { + ensure!( + SolutionProviders::::contains_key(provider_id), + Error::::NoSuchSolutionProvider + ); + + if let Some(solution_provider) = SolutionProviders::::get(provider_id) { + ensure!( + solution_provider.approved, + Error::::SolutionProviderNotApproved + ); + return Ok(().into()); + } + } + Ok(().into()) + } +} diff --git a/substrate-node/pallets/pallet-smart-contract/src/test_utils.rs b/substrate-node/pallets/pallet-smart-contract/src/test_utils.rs index b8368cc8d..795d28641 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/test_utils.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/test_utils.rs @@ -1,5 +1,4 @@ use crate::mock::{PoolState, SmartContractModule, System, Timestamp}; - use frame_support::traits::Hooks; use parity_scale_codec::alloc::sync::Arc; use parking_lot::RwLock; diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index a77429900..a9b3b94d9 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -1,7 +1,7 @@ -use super::{types, Event as SmartContractEvent}; -use crate::cost; -use crate::types::HexHash; -use crate::{mock::RuntimeEvent as MockEvent, mock::*, test_utils::*, Error}; +use crate::{ + cost, mock::RuntimeEvent as MockEvent, mock::*, test_utils::*, types, Error, + Event as SmartContractEvent, +}; use frame_support::{ assert_noop, assert_ok, bounded_vec, dispatch::Pays, @@ -18,8 +18,8 @@ use sp_core::H256; use sp_runtime::{assert_eq_error_rate, traits::SaturatedConversion, Perbill, Percent}; use sp_std::convert::{TryFrom, TryInto}; use substrate_fixed::types::U64F64; -use tfchain_support::constants::time::{SECS_PER_BLOCK, SECS_PER_HOUR}; use tfchain_support::{ + constants::time::{SECS_PER_BLOCK, SECS_PER_HOUR}, resources::Resources, types::{FarmCertification, NodeCertification, PublicIP, IP4}, }; @@ -50,7 +50,7 @@ fn test_create_node_contract_works() { let contract_id = 1; // Ensure contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![contract_id] @@ -494,7 +494,7 @@ fn test_cancel_node_contract_and_remove_from_billing_loop_works() { run_to_block(6, None); // Ensure contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![contract_id] @@ -533,7 +533,7 @@ fn test_remove_from_billing_loop_wrong_index_fails() { let contract_id = 1; // Ensure contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![contract_id] @@ -958,7 +958,7 @@ fn test_node_contract_billing_details() { push_nru_report_for_contract(contract_id, 10); // Ensure contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![contract_id] @@ -1055,7 +1055,7 @@ fn test_node_contract_billing_details_with_solution_provider() { push_nru_report_for_contract(contract_id, 10); // Ensure contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![contract_id] @@ -1113,14 +1113,14 @@ fn test_multiple_contracts_billing_loop_works() { let name_contract_id = 2; // Ensure node_contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(node_contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(node_contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![node_contract_id] ); // Ensure name_contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(name_contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(name_contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![name_contract_id] @@ -1790,7 +1790,7 @@ fn test_name_contract_billing() { let contract_id = 1; // Ensure contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![contract_id] @@ -2509,7 +2509,7 @@ fn test_rent_contract_grace_period_cancels_contract_when_grace_period_ends_works assert_eq!(c1, None); // Ensure contract_id is not in billing loop anymore - let index = SmartContractModule::get_contract_billing_loop_index(contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), Vec::::new() @@ -3876,13 +3876,13 @@ fn test_set_dedicated_node_extra_fee_and_create_rent_contract_billing_works() { let rent_contract = SmartContractModule::contracts(rent_contract_id).unwrap(); // Ensure contract_id is stored at right billing loop index - let index = SmartContractModule::get_contract_billing_loop_index(rent_contract_id); + let index = SmartContractModule::get_billing_loop_index_from_contract_id(rent_contract_id); assert_eq!( SmartContractModule::contract_to_bill_at_block(index), vec![rent_contract_id] ); - let now = Timestamp::get().saturated_into::() / 1000; + let now = SmartContractModule::get_current_timestamp_in_secs(); let mut rent_contract_cost_tft = 0u64; let mut extra_fee_cost_tft = 0; @@ -3912,7 +3912,7 @@ fn test_set_dedicated_node_extra_fee_and_create_rent_contract_billing_works() { .unwrap(); } - let then = Timestamp::get().saturated_into::() / 1000; + let then = SmartContractModule::get_current_timestamp_in_secs(); let seconds_elapsed = then - now; log::debug!("seconds elapsed: {}", seconds_elapsed); @@ -3968,7 +3968,7 @@ macro_rules! test_calculate_discount { // Give just enough balance for targeted number of months at the rate of 1000 per hour let balance = U64F64::from_num(amount_due * 24 * 30) * U64F64::from_num(number_of_months); - let result = cost::calculate_discount::( + let result = cost::calculate_discount_tft::( amount_due, seconds_elapsed, balance.round().to_num::(), @@ -4398,7 +4398,7 @@ fn record(event: RuntimeEvent) -> EventRecord { } } -fn generate_deployment_hash() -> HexHash { +fn generate_deployment_hash() -> types::HexHash { let hash: [u8; 32] = H256::random().to_fixed_bytes(); hash } diff --git a/substrate-node/pallets/pallet-smart-contract/src/types.rs b/substrate-node/pallets/pallet-smart-contract/src/types.rs index 98b5cb24a..ba554a121 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/types.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/types.rs @@ -1,5 +1,7 @@ -use crate::pallet::{MaxDeploymentDataLength, MaxNodeContractPublicIPs}; -use crate::Config; +use crate::{ + pallet::{MaxDeploymentDataLength, MaxNodeContractPublicIPs}, + Config, +}; use core::{convert::TryInto, ops::Add}; use frame_support::{pallet_prelude::ConstU32, BoundedVec, RuntimeDebugNoBound}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; diff --git a/substrate-node/runtime/src/lib.rs b/substrate-node/runtime/src/lib.rs index 6f732d17e..c7ec3b59e 100644 --- a/substrate-node/runtime/src/lib.rs +++ b/substrate-node/runtime/src/lib.rs @@ -401,7 +401,7 @@ impl pallet_smart_contract::Config for Runtime { type AuthorityId = pallet_smart_contract::crypto::AuthId; type Call = RuntimeCall; type MaxNameContractNameLength = MaxNameContractNameLength; - type NameContractName = pallet_smart_contract::name_contract::NameContractName; + type NameContractName = pallet_smart_contract::grid_contract::NameContractName; type RestrictedOrigin = EnsureRootOrCouncilApproval; type MaxDeploymentDataLength = MaxDeploymentDataLength; type MaxNodeContractPublicIps = MaxFarmPublicIps;