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

chain release2.9 compatibility Grace Period and overdue #3387

Open
wants to merge 57 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
4008755
Refactor: add yarn to script to avoid errors
0oM4R Sep 6, 2024
50e3d5a
Feat: add some methods and interfaces
0oM4R Sep 6, 2024
fa43de3
Feat: calculate unique name prcice
0oM4R Sep 6, 2024
593914d
chore: inhance docstirng
0oM4R Sep 6, 2024
8693051
Feat: support contract payment state calculations
0oM4R Sep 6, 2024
9af58da
Refactor: unlock contracts to use new overdue calculations
0oM4R Sep 6, 2024
46b6166
Refactor:
0oM4R Sep 6, 2024
6ae49d0
chore: inhance imports
0oM4R Sep 7, 2024
c655eb6
Refactor: unlock contracts
0oM4R Sep 7, 2024
9bc59cf
add overdue details interfaces
0oM4R Sep 7, 2024
e0cd248
chore: contract overdue
0oM4R Sep 7, 2024
8d72c5c
chore: expose get over due amount by the contract info only
0oM4R Sep 8, 2024
c5f6de8
fix: remove duplicated call
0oM4R Sep 8, 2024
eb1a7fd
refactor:
0oM4R Sep 8, 2024
200b46c
WIP: support overdue changes on contracts list in dashboard
0oM4R Sep 8, 2024
e1d0363
refactor: fix unique name contract
0oM4R Sep 8, 2024
8f2b5b7
wip fix calculations
0oM4R Sep 8, 2024
f29a977
feat: support Payment state
0oM4R Sep 9, 2024
e80eae6
chore: clean up
0oM4R Sep 9, 2024
a496e73
refactor: enhance contract lock state
0oM4R Sep 9, 2024
a1910e2
refactor: enhance contract lock
0oM4R Sep 9, 2024
c161781
refactor: fix is on rented node flag
0oM4R Sep 9, 2024
a946efe
refactor: call the associated rent contract on unlock rent contracts
0oM4R Sep 9, 2024
aa0e95f
feat: show the rent contract that will be unlocked as it is associate…
0oM4R Sep 9, 2024
d1e1889
Style: fix loading spinner in lock dialogs
0oM4R Sep 9, 2024
976695d
fix: build
0oM4R Sep 10, 2024
3eaf6f5
fix: passing contracts to client
0oM4R Sep 10, 2024
ca76686
docs: add deprecated annotation
0oM4R Sep 10, 2024
f650c2f
return the contract cost whatever if it is on a rented node or not
0oM4R Sep 10, 2024
a3f6a6f
docs: WIP adding docstrings
0oM4R Sep 10, 2024
9c1d7d3
WIP: support unlock node contract if the associated rent contract is …
0oM4R Sep 10, 2024
b179374
docs: WIP adding docstrings
0oM4R Sep 11, 2024
524f918
cleanup: remove unused module
0oM4R Sep 11, 2024
accb85b
fix: build
0oM4R Sep 11, 2024
694b6c8
Chore: support unlock node contract if its rent contract is in create…
0oM4R Sep 11, 2024
5721a34
Chore: avoid bill same contract multiple times
0oM4R Sep 11, 2024
1c32ecf
Chore: use currency module to convert usd to tft
0oM4R Sep 17, 2024
b9e0216
refactor:
0oM4R Sep 17, 2024
c16787e
chore: list all contracts
0oM4R Sep 17, 2024
e642604
fix: build
0oM4R Sep 17, 2024
c08b88d
Chore: apply comments
0oM4R Sep 18, 2024
4950681
Chore: support multiple ipv4 per contract
0oM4R Sep 18, 2024
88bf71b
Chore: enahnce code readability
0oM4R Sep 18, 2024
09f8bab
Chore:
0oM4R Sep 18, 2024
9b8f032
Fix: avoid having deleted rent contracts while listing rent contracts
0oM4R Sep 19, 2024
f1b4d55
Fix: pass CRU as number without conversion
0oM4R Sep 19, 2024
ee14685
Chore: add node extra fees to rent contract cost
0oM4R Sep 22, 2024
b2e8914
refactor: include premuim price for certified nodes in the unbuiled nu
0oM4R Sep 22, 2024
60d3ed4
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Sep 30, 2024
69397c3
chore: set decimals to 7 in convert tft price
0oM4R Oct 2, 2024
efb365c
refactor: convert the monthly cost to avoid missing decimals on conve…
0oM4R Oct 2, 2024
cd8d087
fix: get total overdue
0oM4R Oct 7, 2024
829718d
refactor: calculate node contract on rented node
0oM4R Oct 7, 2024
5d63c81
refactor
0oM4R Oct 7, 2024
3abb1a5
refactor:
0oM4R Oct 7, 2024
b2ec2e4
fix: reset total overdue on refresh
0oM4R Oct 8, 2024
8e482a8
reset rentcontract on reset table
0oM4R Oct 8, 2024
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
2 changes: 1 addition & 1 deletion packages/grid_client/scripts/compare_locked_balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async function getUsersWithContracts(grid: GridClient) {

return users;
}

/** @deprecated */
async function getContractsLockedAmount(grid: GridClient, contracts: Contract[]) {
const contractLockDetails = await Promise.all(
contracts.map(contract => grid.contracts.contractLock({ id: +contract.contractID })),
Expand Down
297 changes: 280 additions & 17 deletions packages/grid_client/src/clients/tf-grid/contracts.ts
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";
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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, {
Copy link
Member

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:

  1. The overdue amount for all node contracts associated with a rent contract (total overdraft + unbilled NU cost).
  2. The IP cost for all node contracts per month.

In my opinion, it would be better to handle these calculations with separate functions.

Copy link
Contributor Author

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?

Copy link
Member

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:

  1. getNodeContractsOnRentedNode
  2. calculateNodeContractsOverdue
  3. calculateIPCostPerMonth
    This way, you can retrieve the contracts from the first function and then pass them to the second and third functions.

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;
Copy link
Member

Choose a reason for hiding this comment

The 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 TFT_CONVERSION_FACTOR (maybe in the client) then use it everywhere else for clarity and maintainability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the number of public IPs in the contract instead?
FYI, from the tfchain side, I can create a contract that reserves more than one public IP. Since it is a valid case, despite some client limitations or restrictions, it should be handled when calculating the contract costs as it may be created with different clients.


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;
Copy link
Member

Choose a reason for hiding this comment

The 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);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OverdueTFT name should follow camelCase convention.

return OverdueTFT.add(totalPeriodCost);
}

/**
* WARNING: Please be careful when executing this method, it will delete all your contracts.
* @param {CancelMyContractOptions} options
Expand Down Expand Up @@ -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> {
Expand Down
9 changes: 9 additions & 0 deletions packages/grid_client/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ function convertObjectToQueryString(obj: Record<string, any>): string {

return queryString;
}
/**
* Function to convert from bytes to Gigabytes
* @param {Number} bytes
* @returns {Number}
*/
function bytesToGB(bytes: number) {
return bytes / Math.pow(1024, 3);
}

export {
generateString,
Expand All @@ -95,4 +103,5 @@ export {
generateRandomHexSeed,
zeroPadding,
convertObjectToQueryString,
bytesToGB,
};
16 changes: 16 additions & 0 deletions packages/grid_client/src/modules/calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ class Calculator {
return pricing;
}

/**
* Calculates the cost of a unique name per month.
*
* This function retrieves the price per hour for a unique name, and calculates the total cost in mUSD.
*
*
* @returns {Promise<number>} - The price in mUSD for the unique name usage per month.
*/
@validateInput
async namePricing() {
const uniqueNamePricePerHour = (await this.getPrices()).uniqueName.value;
const priceInUSD = uniqueNamePricePerHour / 10 ** 7;
// return cost per month
return priceInUSD * 24 * 30;
}

/**
* Asynchronously retrieves the TFT price from the TFChain.
*
Expand Down
Loading
Loading