Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pallet): allow farmer to set extra fee on its nodes #726

Merged
merged 27 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
95f8880
feat: dedicated node extra fee
renauter Jun 7, 2023
bb6d2f0
chore: merge development
DylanVerstraete Jun 7, 2023
75bbda4
fix: ci
DylanVerstraete Jun 7, 2023
99359cc
feat: rework check before creating node contract
DylanVerstraete Jun 7, 2023
03e3e94
chore: rework syntax
DylanVerstraete Jun 7, 2023
9ca99fa
chore: revert farmer_share concept
renauter Jun 7, 2023
8e6295d
feat: dedicated node extra fee billing
renauter Jun 7, 2023
f37d57a
test: set dedicated node extra fee
renauter Jun 7, 2023
5d2483b
feat: add migration for locks
DylanVerstraete Jun 8, 2023
29d230a
chore: merge development
DylanVerstraete Jun 8, 2023
4abcd0b
chore: complete adr
DylanVerstraete Jun 8, 2023
5d4fe9a
fix: some general fixes and refactor
DylanVerstraete Jun 8, 2023
92c74d7
fix: rework test
DylanVerstraete Jun 9, 2023
0f00251
fix: go client bill event
DylanVerstraete Jun 9, 2023
c02830a
feat: add js client methods
DylanVerstraete Jun 9, 2023
4cadce0
feat: add go client methods
DylanVerstraete Jun 9, 2023
06950b2
fix: js client
DylanVerstraete Jun 9, 2023
7a14d2f
fix: go integration tests
DylanVerstraete Jun 9, 2023
8924cf1
chore: revert contract bill event type
DylanVerstraete Jun 12, 2023
ab050c0
chore: remove println
DylanVerstraete Jun 12, 2023
3bfe18c
chore: merge development
DylanVerstraete Jun 12, 2023
ebe5870
reviews and test rework
renauter Jun 12, 2023
1d8b942
chore: rework migration v11
renauter Jun 12, 2023
1ceaad4
feat: round costs before converting back to integer
renauter Jun 12, 2023
f41931f
chore: remove todo comment
renauter Jun 13, 2023
930f5cc
Merge remote-tracking branch 'origin' into development_feat_node_extr…
renauter Jun 13, 2023
e1cb885
chore: merge development
DylanVerstraete Jun 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions clients/tfchain-client-go/contract_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,10 @@ type BillingFrequencyChanged struct {
Frequency types.U64 `json:"frequency"`
Topics []types.Hash
}

type NodeExtraFeeSet struct {
Phase types.Phase
NodeID types.U32 `json:"node_id"`
ExtraFee types.U64 `json:"extra_fee"`
Topics []types.Hash
}
1 change: 1 addition & 0 deletions clients/tfchain-client-go/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ type EventRecords struct {
SmartContractModule_ServiceContractCanceled []ServiceContractCanceled //nolint:stylecheck,golint
SmartContractModule_ServiceContractBilled []ServiceContractBilled //nolint:stylecheck,golint
SmartContractModule_BillingFrequencyChanged []BillingFrequencyChanged //nolint:stylecheck,golint
SmartContractModule_NodeExtraFeeSet []NodeExtraFeeSet //nolint:stylecheck,golint

// farm events
TfgridModule_FarmStored []FarmStored //nolint:stylecheck,golint
Expand Down
2 changes: 1 addition & 1 deletion clients/tfchain-client-go/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ var smartContractModuleErrors = []string{
"MethodIsDeprecated",
"NodeHasActiveContracts",
"NodeHasRentContract",
"NodeIsNotDedicated",
"FarmIsNotDedicated",
"NodeNotAvailableToDeploy",
"CannotUpdateContractInGraceState",
"NumOverflow",
Expand Down
15 changes: 15 additions & 0 deletions docs/architecture/0011-dedicated-nodes-extra-fee.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 11. Dedicated nodes extra fee

Date: 2023-06-06

## Status

Accepted

## Context

TODO

## Decision

TODO
44 changes: 31 additions & 13 deletions substrate-node/pallets/pallet-smart-contract/src/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::pallet::Error;
use crate::types;
use crate::types::{Contract, ContractBillingInformation, ServiceContract, ServiceContractBill};
use crate::Config;
use crate::DedicatedNodesExtraFee;
use frame_support::{dispatch::DispatchErrorWithPostInfo, traits::Get};
use pallet_tfgrid::types as pallet_tfgrid_types;
use sp_runtime::{Percent, SaturatedConversion};
Expand All @@ -21,7 +22,7 @@ impl<T: Config> Contract<T> {
&self,
balance: BalanceOf<T>,
seconds_elapsed: u64,
) -> Result<(BalanceOf<T>, types::DiscountLevel), DispatchErrorWithPostInfo> {
) -> Result<(BalanceOf<T>, u8, types::DiscountLevel), DispatchErrorWithPostInfo> {
// Fetch the default pricing policy and certification type
let pricing_policy = pallet_tfgrid::PricingPolicies::<T>::get(1).unwrap();
let certification_type = NodeCertification::Diy;
Expand All @@ -30,11 +31,13 @@ impl<T: Config> Contract<T> {
// - NodeContract
// - RentContract
// - NameContract
let total_cost = self.calculate_contract_cost(&pricing_policy, seconds_elapsed)?;
let (total_cost, farmer_share) =
self.calculate_contract_cost(&pricing_policy, seconds_elapsed)?;
// If cost is 0, reinsert to be billed at next interval
if total_cost == 0 {
return Ok((
BalanceOf::<T>::saturated_from(0 as u128),
0,
types::DiscountLevel::None,
));
}
Expand All @@ -49,26 +52,26 @@ impl<T: Config> Contract<T> {
certification_type,
);

return Ok((amount_due, discount_received));
return Ok((amount_due, farmer_share, discount_received));
}

pub fn calculate_contract_cost(
&self,
pricing_policy: &pallet_tfgrid_types::PricingPolicy<T::AccountId>,
seconds_elapsed: u64,
) -> Result<u64, DispatchErrorWithPostInfo> {
) -> Result<(u64, u8), DispatchErrorWithPostInfo> {
let mut farmer_share = 0u8;
let total_cost = match &self.contract_type {
// Calculate total cost for a node contract
types::ContractData::NodeContract(node_contract) => {
// Get the contract billing info to view the amount unbilled for NRU (network resource units)
let contract_billing_info = self.get_billing_info();
// Get the node
if !pallet_tfgrid::Nodes::<T>::contains_key(node_contract.node_id) {
// Make sure the node exists
if pallet_tfgrid::Nodes::<T>::get(node_contract.node_id).is_none() {
return Err(DispatchErrorWithPostInfo::from(Error::<T>::NodeNotExists));
}

// We know the contract is using resources, now calculate the cost for each used resource

let node_contract_resources =
pallet::Pallet::<T>::node_contract_resources(self.contract_id);

Expand All @@ -89,10 +92,8 @@ impl<T: Config> Contract<T> {
contract_cost + contract_billing_info.amount_unbilled
}
types::ContractData::RentContract(rent_contract) => {
if !pallet_tfgrid::Nodes::<T>::contains_key(rent_contract.node_id) {
return Err(DispatchErrorWithPostInfo::from(Error::<T>::NodeNotExists));
}
let node = pallet_tfgrid::Nodes::<T>::get(rent_contract.node_id).unwrap();
let node = pallet_tfgrid::Nodes::<T>::get(rent_contract.node_id)
.ok_or(Error::<T>::NodeNotExists)?;

let contract_cost = calculate_resources_cost::<T>(
node.resources,
Expand All @@ -101,7 +102,24 @@ impl<T: Config> Contract<T> {
&pricing_policy,
true,
);
Percent::from_percent(pricing_policy.discount_for_dedication_nodes) * contract_cost
let regular_cost =
Percent::from_percent(pricing_policy.discount_for_dedication_nodes)
* contract_cost;
let extra_fee_cost = match DedicatedNodesExtraFee::<T>::get(rent_contract.node_id) {
Some(fee_per_month) => {
//
let extra_cost = (U64F64::from_num(fee_per_month * seconds_elapsed)
/ U64F64::from_num(30 * 24 * 60 * 60)) // seconds in 1 month
.to_num::<u64>();
let cost_with_extra = regular_cost + extra_cost;
farmer_share = (U64F64::from_num(extra_cost) * 100
/ U64F64::from_num(cost_with_extra))
.to_num::<u8>();
extra_cost
}
_ => 0,
};
regular_cost + extra_fee_cost // TO CHECK: apply discount to extra fee?
}
// Calculate total cost for a name contract
types::ContractData::NameContract(_) => {
Expand All @@ -113,7 +131,7 @@ impl<T: Config> Contract<T> {
}
};

Ok(total_cost)
Ok((total_cost, farmer_share))
}
}

Expand Down
87 changes: 75 additions & 12 deletions substrate-node/pallets/pallet-smart-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ pub mod pallet {
#[pallet::getter(fn current_migration_stage)]
pub(super) type CurrentMigrationStage<T: Config> = StorageValue<_, MigrationStage, OptionQuery>;

#[pallet::storage]
#[pallet::getter(fn dedicated_nodes_extra_fee)]
pub type DedicatedNodesExtraFee<T> = StorageMap<_, Blake2_128Concat, u32, u64, OptionQuery>;

#[pallet::config]
pub trait Config:
CreateSignedTransaction<Call<Self>>
Expand Down Expand Up @@ -343,6 +347,10 @@ pub mod pallet {
amount: BalanceOf<T>,
},
BillingFrequencyChanged(u64),
NodeExtraFeeSet {
node_id: u32,
extra_fee: u64,
},
}

#[pallet::error]
Expand Down Expand Up @@ -371,7 +379,7 @@ pub mod pallet {
MethodIsDeprecated,
NodeHasActiveContracts,
NodeHasRentContract,
NodeIsNotDedicated,
FarmIsNotDedicated,
NodeNotAvailableToDeploy,
CannotUpdateContractInGraceState,
NumOverflow,
Expand All @@ -398,6 +406,7 @@ pub mod pallet {
IsNotAnAuthority,
WrongAuthority,
UnauthorizedToChangeSolutionProviderId,
UnauthorizedToSetExtraFee,
}

#[pallet::genesis_config]
Expand Down Expand Up @@ -650,6 +659,17 @@ pub mod pallet {
let account_id = ensure_signed(origin)?;
Self::_attach_solution_provider_id(account_id, contract_id, solution_provider_id)
}

#[pallet::call_index(20)]
#[pallet::weight(100_000_000 + T::DbWeight::get().writes(1).ref_time() + T::DbWeight::get().reads(1).ref_time())]
pub fn set_dedicated_node_extra_fee(
origin: OriginFor<T>,
node_id: u32,
extra_fee: u64,
) -> DispatchResultWithPostInfo {
let account_id = ensure_signed(origin)?;
Self::_set_dedicated_node_extra_fee(account_id, node_id, extra_fee)
}
}

#[pallet::hooks]
Expand Down Expand Up @@ -737,18 +757,17 @@ impl<T: Config> Pallet<T> {

let farm = pallet_tfgrid::Farms::<T>::get(node.farm_id).ok_or(Error::<T>::FarmNotExists)?;

if farm.dedicated_farm && !ActiveRentContractForNode::<T>::contains_key(node_id) {
return Err(Error::<T>::NodeNotAvailableToDeploy.into());
}

// If the user is trying to deploy on a node that has an active rent contract
// only allow the user who created the rent contract to actually deploy a node contract on it
let mut is_node_owner = false;
if let Some(contract_id) = ActiveRentContractForNode::<T>::get(node_id) {
let rent_contract =
Contracts::<T>::get(contract_id).ok_or(Error::<T>::ContractNotExists)?;
if rent_contract.twin_id != twin_id {
return Err(Error::<T>::NodeHasRentContract.into());
}
is_node_owner = rent_contract.twin_id == twin_id;
renauter marked this conversation as resolved.
Show resolved Hide resolved
}

let fee = DedicatedNodesExtraFee::<T>::get(node_id).unwrap_or(0);
renauter marked this conversation as resolved.
Show resolved Hide resolved
// If the user is not the owner of the node and the node is not a dedicated node then we don't allow the creation of it.
renauter marked this conversation as resolved.
Show resolved Hide resolved
if !is_node_owner && farm.dedicated_farm || !is_node_owner && fee > 0 {
return Err(Error::<T>::NodeNotAvailableToDeploy.into());
renauter marked this conversation as resolved.
Show resolved Hide resolved
}

// If the contract with hash and node id exists and it's in any other state then
Expand Down Expand Up @@ -1173,7 +1192,7 @@ impl<T: Config> Pallet<T> {
let mut contract_lock = ContractLock::<T>::get(contract.contract_id);
let seconds_elapsed = now.checked_sub(contract_lock.lock_updated).unwrap_or(0);

let (amount_due, discount_received) =
let (amount_due, farmer_share, discount_received) =
contract.calculate_contract_cost_tft(total_balance, seconds_elapsed)?;

// If there is nothing to be paid and the contract is not in state delete, return
Expand Down Expand Up @@ -1204,7 +1223,7 @@ impl<T: Config> Pallet<T> {
}

// Handle contract lock operations
Self::handle_lock(contract, &mut contract_lock, amount_due)?;
Self::handle_lock(contract, &mut contract_lock, amount_due, farmer_share)?;

// Always emit a contract billed event
let contract_bill = types::ContractBill {
Expand Down Expand Up @@ -1347,6 +1366,7 @@ impl<T: Config> Pallet<T> {
contract: &mut types::Contract<T>,
contract_lock: &mut types::ContractLock<BalanceOf<T>>,
amount_due: BalanceOf<T>,
farmer_share: u8,
) -> DispatchResultWithPostInfo {
let now = <timestamp::Pallet<T>>::get().saturated_into::<u64>() / 1000;

Expand Down Expand Up @@ -1404,6 +1424,7 @@ impl<T: Config> Pallet<T> {
&contract,
&pricing_policy,
twin_balance.min(contract_lock.amount_locked),
farmer_share,
) {
Ok(_) => {}
Err(err) => {
Expand Down Expand Up @@ -1481,6 +1502,7 @@ impl<T: Config> Pallet<T> {
contract: &types::Contract<T>,
pricing_policy: &pallet_tfgrid_types::PricingPolicy<T::AccountId>,
amount: BalanceOf<T>,
_farmer_share: u8,
) -> DispatchResultWithPostInfo {
log::info!(
"Distributing cultivation rewards for contract {:?} with amount {:?}",
Expand All @@ -1497,6 +1519,9 @@ impl<T: Config> Pallet<T> {
let twin =
pallet_tfgrid::Twins::<T>::get(contract.twin_id).ok_or(Error::<T>::TwinNotExists)?;

// TODO: extract farmer share from amount and transfer it to farmer
// from here distribute the remaining amount following the regular distribution schema

// Send 10% to the foundation
let foundation_share = Perbill::from_percent(10) * amount;
log::debug!(
Expand Down Expand Up @@ -2383,6 +2408,44 @@ impl<T: Config> Pallet<T> {

Ok(().into())
}

pub fn _set_dedicated_node_extra_fee(
account_id: T::AccountId,
node_id: u32,
extra_fee: u64,
) -> DispatchResultWithPostInfo {
// Nothing to do if fee value is 0
if extra_fee == 0 {
return Ok(().into());
}

// Make sure only the farmer that owns this node can set the extra fee
let twin_id = pallet_tfgrid::TwinIdByAccountID::<T>::get(&account_id)
.ok_or(Error::<T>::TwinNotExists)?;
let node = pallet_tfgrid::Nodes::<T>::get(node_id).ok_or(Error::<T>::NodeNotExists)?;
let farm = pallet_tfgrid::Farms::<T>::get(node.farm_id).ok_or(Error::<T>::FarmNotExists)?;
ensure!(
twin_id == farm.twin_id,
Error::<T>::UnauthorizedToSetExtraFee
);

// Make sure farm is dedicated so it blocks running a node contract
// on a node unless there is an initial rent contract on it
ensure!(farm.dedicated_farm, Error::<T>::FarmIsNotDedicated);

// Make sure there is no active node or rent contract on this node
ensure!(
ActiveRentContractForNode::<T>::get(node_id).is_none()
&& ActiveNodeContracts::<T>::get(&node_id).is_empty(),
Error::<T>::NodeHasActiveContracts
);

// Set fee in units USD (converted from mUSD)
DedicatedNodesExtraFee::<T>::insert(node_id, extra_fee * 10000);
Self::deposit_event(Event::NodeExtraFeeSet { node_id, extra_fee });

Ok(().into())
}
}

impl<T: Config> ChangeNode<LocationOf<T>, InterfaceOf<T>, SerialNumberOf<T>> for Pallet<T> {
Expand Down
Loading