-
Notifications
You must be signed in to change notification settings - Fork 8
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
chain release2.9 compatibility Grace Period and overdue #3387
base: development
Are you sure you want to change the base?
Changes from 42 commits
4008755
50e3d5a
fa43de3
593914d
8693051
9af58da
46b6166
6ae49d0
c655eb6
9bc59cf
e0cd248
8d72c5c
c5f6de8
eb1a7fd
200b46c
e1d0363
8f2b5b7
f29a977
e80eae6
a496e73
a1910e2
c161781
a946efe
aa0e95f
d1e1889
976695d
3eaf6f5
ca76686
f650c2f
a3f6a6f
9c1d7d3
b179374
524f918
accb85b
694b6c8
5721a34
1c32ecf
b9e0216
c16787e
e642604
c08b88d
4950681
88bf71b
09f8bab
9b8f032
f1b4d55
ee14685
b2e8914
60d3ed4
69397c3
efb365c
cd8d087
829718d
5d63c81
3abb1a5
b2ec2e4
8e482a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,26 @@ | ||
import { | ||
import GridProxyClient, { | ||
CertificationType, | ||
Contract, | ||
ContractsQuery, | ||
ContractState, | ||
ContractType, | ||
} from "@threefold/gridproxy_client"; | ||
import { | ||
BillingInformation, | ||
ContractLock, | ||
ContractLockOptions, | ||
ContractPaymentState, | ||
Contracts, | ||
ExtrinsicResult, | ||
GetDedicatedNodePriceOptions, | ||
NodeContractUsedResources, | ||
SetDedicatedNodeExtraFeesOptions, | ||
} from "@threefold/tfchain_client"; | ||
import { GridClientError } from "@threefold/types"; | ||
import { Decimal } from "decimal.js"; | ||
|
||
import { formatErrorMessage } from "../../helpers"; | ||
import { ContractStates } from "../../modules"; | ||
import { bytesToGB, formatErrorMessage } from "../../helpers"; | ||
import { calculator, ContractStates, currency } from "../../modules"; | ||
import { Graphql } from "../graphql/client"; | ||
|
||
export type DiscountLevel = "None" | "Default" | "Bronze" | "Silver" | "Gold"; | ||
|
@@ -110,6 +120,21 @@ export interface LockContracts { | |
totalAmountLocked: number; | ||
} | ||
|
||
export type OverdueDetails = { [key: number]: number }; | ||
|
||
export interface ContractsOverdue { | ||
nameContracts: OverdueDetails; | ||
nodeContracts: OverdueDetails; | ||
rentContracts: OverdueDetails; | ||
totalOverdueAmount: number; | ||
} | ||
export interface CalculateOverdueOptions { | ||
contractInfo: Contract; | ||
gridProxyClient: GridProxyClient; | ||
} | ||
|
||
const SECONDS_ONE_HOUR = 60 * 60; | ||
|
||
class TFContracts extends Contracts { | ||
async listContractsByTwinId(options: ListContractByTwinIdOptions): Promise<GqlContracts> { | ||
options.stateList = options.stateList || [ContractStates.Created, ContractStates.GracePeriod]; | ||
|
@@ -301,13 +326,219 @@ class TFContracts extends Contracts { | |
}); | ||
} | ||
|
||
/** @deprecated */ | ||
async contractLock(options: ContractLockOptions) { | ||
const res = await super.contractLock(options); | ||
const amountLocked = new Decimal(res.amountLocked); | ||
res.amountLocked = amountLocked.div(10 ** 7).toNumber(); | ||
return res; | ||
} | ||
|
||
/** | ||
* Function to convert USD to TFT | ||
* @param {Decimal} USD the amount in USD. | ||
* @returns {Decimal} The amount in TFT. | ||
*/ | ||
private async convertToTFT(USD: Decimal) { | ||
try { | ||
const tftPrice = (await this.client.tftPrice.get()) ?? 0; | ||
const tft = new currency(tftPrice).convertUSDtoTFT({ amount: USD.toNumber() }); | ||
return new Decimal(tft); | ||
} catch (error) { | ||
throw new GridClientError(`Failed to convert to mTFT due: ${error}`); | ||
} | ||
} | ||
/** | ||
* list all contracts, this is not restricted with the items counts | ||
* this basically check if the count is larger than the page size, it make another request with the item count as the size pram. | ||
* @param {GridProxyClient} proxy will be used to list the contracts | ||
* @param {Partial<ContractsQuery>} queries | ||
* @returns | ||
*/ | ||
async listAllContracts(proxy: GridProxyClient, queries: Partial<ContractsQuery>) { | ||
const contracts = await proxy.contracts.list({ ...queries, retCount: true }); | ||
if (contracts.data.length < contracts.count!) { | ||
return ( | ||
await proxy.contracts.list({ | ||
...queries, | ||
size: contracts.count!, | ||
}) | ||
).data; | ||
} else return contracts.data; | ||
} | ||
|
||
/** | ||
* Calculate total IPV4 and total overdraft on all node contracts on rented node. | ||
* @description In node contracts on rented nodes we need to have both the overdraft to be added to rent contract overdraft. | ||
* please note that the unbilled NU amount is added to the total overdraft exactly as the in the other contract types. | ||
* the IPV4 cost to add to the estimated cost of the rent contract. | ||
*/ | ||
private async getContractsCostOnRentedNode(nodeId: number, proxy: GridProxyClient) { | ||
const contracts = await this.listAllContracts(proxy, { | ||
nodeId, | ||
state: [ContractState.GracePeriod], | ||
numberOfPublicIps: 1, | ||
}); | ||
const publicIpsCount = contracts.reduce((acc, contract) => acc + (contract.details.number_of_public_ips || 0), 0); | ||
// will convert from Unit USD to USD inline; | ||
const ipPrice = (await this.client.pricingPolicies.get({ id: 1 })).ipu.value / 10 ** 7; | ||
const BillingInformationPromises = contracts.reduce((acc: Promise<BillingInformation>[], contract) => { | ||
acc.push(this.client.contracts.getContractBillingInformationByID(contract.contract_id)); | ||
return acc; | ||
}, []); | ||
const overDraftPromises = contracts.reduce((acc: Promise<ContractPaymentState>[], contract) => { | ||
acc.push(this.client.contracts.getContractPaymentState(contract.contract_id)); | ||
return acc; | ||
}, []); | ||
|
||
const overDraftResult = await Promise.all(overDraftPromises); | ||
const billingInfoResult = await Promise.all(BillingInformationPromises); | ||
|
||
const totalOverDraft = overDraftResult.reduce( | ||
(acc: Decimal, paymentState) => acc.add(paymentState.additionalOverdraft).add(paymentState.standardOverdraft), | ||
new Decimal(0), | ||
); | ||
const totalNUCost = billingInfoResult.reduce((acc: number, billingInfo) => acc + billingInfo.amountUnbilled, 0); | ||
const totalNUCostTFTUnit = await this.convertToTFT(new Decimal(totalNUCost)); | ||
const totalIPCost = ipPrice * publicIpsCount; | ||
return { | ||
//return ip cost per month | ||
totalIpCost: totalIPCost * 24 * 30, | ||
totalOverdraft: totalOverDraft.add(totalNUCostTFTUnit), | ||
}; | ||
} | ||
|
||
/** | ||
* This function is for calculating the estimated cost of the contract per month. | ||
* @description | ||
* Name contract cost is fixed price for the unique name, | ||
* Rent contract cost is the cost of the total node resources, | ||
* Node contract have two cases: | ||
* 1- on rented contract, this case the cost will be only for the ipv4. | ||
* 2- on shared node, this will be the shared price of the used resources | ||
|
||
* @param {Contract} contract | ||
* @param {GridProxyClient} proxy | ||
* @returns the cost of the contract per month in USD | ||
*/ | ||
async getContractCost(contract: Contract, proxy: GridProxyClient) { | ||
const calc = new calculator(this.client); | ||
|
||
if (contract.type == ContractType.Name) return await calc.namePricing(); | ||
|
||
//TODO allow ipv4 to be number | ||
|
||
/** Other contract types need the node information */ | ||
const nodeDetails = await proxy.nodes.byId(contract.details.nodeId); | ||
|
||
const isCertified = nodeDetails.certificationType == CertificationType.Certified ? true : false; | ||
if (contract.type == ContractType.Rent) { | ||
const { cru, sru, mru, hru } = nodeDetails.total_resources; | ||
const USDCost = ( | ||
await calc.calculateWithMyBalance({ | ||
ipv4u: false, | ||
certified: isCertified, | ||
cru: bytesToGB(cru), | ||
mru: bytesToGB(mru), | ||
hru: bytesToGB(hru), | ||
sru: bytesToGB(sru), | ||
}) | ||
).dedicatedPrice; | ||
|
||
return USDCost; | ||
} | ||
|
||
/** Node Contract */ | ||
|
||
/** Node contract on rented node | ||
* If the node contract has IPV4 will return the price of the ipv4 per month | ||
* If not there is no cost, will return zero | ||
*/ | ||
|
||
if (nodeDetails.rented) { | ||
if (!contract.details.number_of_public_ips) return 0; | ||
|
||
const ipPrice = (await this.client.pricingPolicies.get({ id: 1 })).ipu.value / 10 ** 7; | ||
return ipPrice * 30 * 24; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would avoid using magic numbers like 10 ** 7 and define it as constant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add for the current file, will open an issue to make it global value for the grid client |
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use the number of public IPs in the contract instead? |
||
|
||
const usedREsources: NodeContractUsedResources = await this.client.contracts.getNodeContractResources({ | ||
id: contract.contract_id, | ||
}); | ||
const { cru, sru, mru, hru } = usedREsources.used; | ||
const USDCost = ( | ||
await calc.calculateWithMyBalance({ | ||
ipv4u: !!contract.details.number_of_public_ips, | ||
certified: isCertified, | ||
cru: bytesToGB(cru), | ||
mru: bytesToGB(mru), | ||
hru: bytesToGB(hru), | ||
sru: bytesToGB(sru), | ||
}) | ||
).sharedPrice; | ||
|
||
return await USDCost; | ||
} | ||
|
||
/** | ||
* Calculates the overdue amount for a contract. | ||
* | ||
* @description This method calculates the overdue amount, the overdue amount basically is the sum of three parts: | ||
* 1- Total over draft: is the sum of additional overdraft and standard overdraft. | ||
* 2- Unbilled NU: is the unbilled amount of network usage. | ||
* 3- The estimated cost of the contract for the total period: this part is dependant on the contract type and if the contract is on rented node or not. | ||
* If the contract is rent contract, will add both of ipv4 cost and the total overdue of all associated contracts. | ||
* The total period is the time since the last billing added to Allowance period. | ||
* The resulting overdue amount represents the amount that needs to be addressed. | ||
* | ||
* @param {CalculateOverdueOptions} options - The options containing the contract and gridProxyClient. | ||
* @returns {Promise<number>} - The calculated overdue amount in TFT. | ||
*/ | ||
async calculateContractOverDue(options: CalculateOverdueOptions) { | ||
const contractInfo = options.contractInfo; | ||
|
||
/** Get the Un-billed amount in unit USD for the network usage */ | ||
const unbilledNU = (await this.client.contracts.getContractBillingInformationByID(contractInfo.contract_id)) | ||
.amountUnbilled; | ||
const unbilledNUTFTUnit = await this.convertToTFT(new Decimal(unbilledNU)); | ||
|
||
const { standardOverdraft, additionalOverdraft, lastUpdatedSeconds } = | ||
await this.client.contracts.getContractPaymentState(contractInfo.contract_id); | ||
|
||
/**Calculate the elapsed seconds since last pilling*/ | ||
const elapsedSeconds = Date.now() / 1000 - lastUpdatedSeconds; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a small typo here "pilling" -> "billing" |
||
|
||
let contractMonthlyCost = new Decimal(await this.getContractCost(contractInfo, options.gridProxyClient)); | ||
/**Calculate total over overDraft added to the NU unbilled amount*/ | ||
let totalOverDraft = new Decimal(standardOverdraft).add(additionalOverdraft); | ||
|
||
if (contractInfo.type == ContractType.Rent) { | ||
// get the contracts on the rented node, calculate the nu, then add the ipv4 cost | ||
const totalContractsCost = await this.getContractsCostOnRentedNode( | ||
contractInfo.details.nodeId, | ||
options.gridProxyClient, | ||
); | ||
totalOverDraft = totalOverDraft.add(totalContractsCost.totalOverdraft); | ||
contractMonthlyCost = contractMonthlyCost.add(totalContractsCost.totalIpCost); | ||
} | ||
|
||
// time since the last billing with allowance time of **one hour** | ||
const totalPeriodTime = elapsedSeconds + SECONDS_ONE_HOUR; | ||
|
||
//USD | ||
const contractCostPerSecond = contractMonthlyCost.div(30 * 24 * SECONDS_ONE_HOUR); | ||
const contractCostTFT = await this.convertToTFT(contractCostPerSecond); | ||
|
||
// cost of the current billing period with the mentioned allowance time | ||
const totalPeriodCost = contractCostTFT.times(totalPeriodTime); | ||
const overdue = totalOverDraft.add(unbilledNUTFTUnit); | ||
|
||
//convert to TFT | ||
const OverdueTFT = overdue.div(10 ** 7); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return OverdueTFT.add(totalPeriodCost); | ||
} | ||
|
||
/** | ||
* WARNING: Please be careful when executing this method, it will delete all your contracts. | ||
* @param {CancelMyContractOptions} options | ||
|
@@ -335,28 +566,60 @@ class TFContracts extends Contracts { | |
await this.client.applyAllExtrinsics(extrinsics); | ||
return ids; | ||
} | ||
|
||
async batchUnlockContracts(ids: number[]) { | ||
const billableContractsIDs: number[] = []; | ||
for (const id of ids) { | ||
if ((await this.contractLock({ id })).amountLocked > 0) billableContractsIDs.push(id); | ||
/** | ||
* Async function that request to resume the passed contracts. | ||
* | ||
* @description | ||
* This function create array of `ExtrinsicResult<number>` to use in `applyAllExtrinsics`. | ||
* It's not guaranteed that the contracts will be resumed; It just trigger billing request; if it pass the contract will be resumed. | ||
* the function will ignore all contracts that do not have overdue, also if there is sum rent contracts, its associated node contracts that have ipv4 will be added. | ||
* @param {Contract[]} contracts contracts to be | ||
* @param {GridProxyClient} proxy | ||
* @returns {number[]} contract ids that have been requested to resume | ||
*/ | ||
async batchUnlockContracts(contracts: Contract[], proxy: GridProxyClient) { | ||
const billableContractsIDs: Set<number> = new Set(); | ||
for (const contract of contracts) { | ||
const contractOverdue = ( | ||
await this.calculateContractOverDue({ contractInfo: contract, gridProxyClient: proxy }) | ||
).toNumber(); | ||
if (contractOverdue > 0) { | ||
billableContractsIDs.add(contract.contract_id); | ||
|
||
if (contract.type == ContractType.Rent) { | ||
/** add associated node contracts on the rented node `with public ip` to the contracts to bill */ | ||
const nodeContracts = await this.listAllContracts(proxy, { | ||
numberOfPublicIps: 1, | ||
state: [ContractState.GracePeriod], | ||
nodeId: contract.details.nodeId, | ||
}); | ||
nodeContracts.forEach(contract => billableContractsIDs.add(contract.contract_id)); | ||
} | ||
} | ||
} | ||
const extrinsics: ExtrinsicResult<number>[] = []; | ||
for (const id of billableContractsIDs) { | ||
for (const id of Array.from(billableContractsIDs)) { | ||
extrinsics.push(await this.unlock(id)); | ||
} | ||
return this.client.applyAllExtrinsics(extrinsics); | ||
} | ||
|
||
async unlockMyContracts(graphqlURL: string) { | ||
const contracts = await this.listMyContracts({ | ||
stateList: [ContractStates.GracePeriod], | ||
graphqlURL, | ||
/** | ||
* Request to resume all grace period contracts associated with the current twinId | ||
* @description | ||
* This function lists all grace period contracts, then call {@link batchUnlockContracts}. | ||
* @param {String} gridProxyUrl | ||
* @returns contract ids that have been requested to resume. | ||
*/ | ||
async unlockMyContracts(gridProxyUrl: string) { | ||
const proxy = new GridProxyClient(gridProxyUrl); | ||
const contracts = await this.listAllContracts(proxy, { | ||
state: [ContractState.GracePeriod], | ||
twinId: await this.client.twins.getMyTwinId(), | ||
}); | ||
const ids: number[] = [...contracts.nameContracts, ...contracts.nodeContracts, ...contracts.rentContracts].map( | ||
contract => parseInt(contract.contractID), | ||
); | ||
return await this.batchUnlockContracts(ids); | ||
|
||
if (contracts.length == 0) return []; | ||
return await this.batchUnlockContracts(contracts as Contract[], proxy); | ||
} | ||
|
||
async getDedicatedNodeExtraFee(options: GetDedicatedNodePriceOptions): Promise<number> { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function calculates two separate costs:
In my opinion, it would be better to handle these calculations with separate functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I put both of them in one place to reduce listing the contracts again, here already have the contracts and their count, i was thinking about move the ip calculations to separate function and call it here but this will change nothing imo, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because this function does a lot of things.
What about splitting it into three separate functions?
The three new functions would be:
getNodeContractsOnRentedNode
calculateNodeContractsOverdue
calculateIPCostPerMonth
This way, you can retrieve the contracts from the first function and then pass them to the second and third functions.