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

Merged
merged 63 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 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
7026c2c
Fix: add premium price for ip price on rent node
0oM4R Oct 9, 2024
0714187
Chore: remove unused functions
0oM4R Oct 9, 2024
aed21b5
Chore: set to fixed point 15 and fix the error msg
0oM4R Oct 10, 2024
0a4aab6
Chore: include node contract on rented node in the getTotalOverDue fu…
0oM4R Oct 10, 2024
8667aa0
Style: rephrase unlock dialog msg
0oM4R Oct 14, 2024
5b4536f
merge development
0oM4R Oct 31, 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
338 changes: 321 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 @@ -109,6 +119,40 @@ 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;
}

/**
* Represents the total cost associated with the provided contracts.
*
* @interface TotalContractsCost
*
* @property {number} ipsCost - Total cost for the provided amount for ips per mount in USD.
* @property {Decimal} nuCost - The total unbilled amount of network usage (NU), represented as a Decimal Unit USD.
* @property {Decimal} overdraft - The overdraft amount, the sum of `additionalOverdraft` and `standardOverdraft` represented as a Decimal as Unit TFT.
*/
interface TotalContractsCost {
ipsCost: number;
nuCost: number;
overdraft: Decimal;
}

const SECONDS_ONE_HOUR = 60 * 60;

const HOURS_ONE_MONTH = 24 * 30;

const TFT_CONVERSION_FACTOR = 10 ** 7;

class TFContracts extends Contracts {
async listContractsByTwinId(options: ListContractByTwinIdOptions): Promise<GqlContracts> {
options.stateList = options.stateList || [ContractStates.Created, ContractStates.GracePeriod];
Expand Down Expand Up @@ -297,13 +341,241 @@ 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, 15).convertUSDtoTFT({ amount: USD.toNumber() });
return new Decimal(tft);
} catch (error) {
throw new GridClientError(`Failed to convert to TFT 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;
}

/**
* List all the grace period contracts on a node
* @param {number} nodeId rented node to list its contracts
* @param {GridProxyClient} proxy
* @returns {Contract[]}
*/
private async getNodeContractsOnRentedNode(nodeId: number, proxy: GridProxyClient): Promise<Contract[]> {
return await this.listAllContracts(proxy, {
nodeId,
state: [ContractState.GracePeriod],
numberOfPublicIps: 1,
});
}

/**
Copy link
Member

Choose a reason for hiding this comment

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

Can we handle the case where there are no returned contracts and return early?

* Get the contract billing info, and add the additional price markup if the node is certified
* @param {Contract} contract contract to get its billing info
* @returns {Decimal}
*/
private async getUnbilledNu(contract_id: number, node_id: number) {
const billingInfo = await this.client.contracts.getContractBillingInformationByID(contract_id);
const unbilledNU = billingInfo.amountUnbilled;
if (unbilledNU > 0) {
const nodeInfo = await this.client.nodes.get({ id: node_id });
const isCertified = nodeInfo.certification === CertificationType.Certified;
if (isCertified) {
/** premium pricing is 25% on certified nodes */
const premiumUnbilledNU = unbilledNU * (125 / 100);
return premiumUnbilledNU;
}
}
return unbilledNU;
}

/**
* Calculate contract cost for all node contracts on rented node.
* @description will list all node contracts with public ip and calculate all contracts overdue .
* 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): Promise<Decimal> {
const contracts = await this.getNodeContractsOnRentedNode(nodeId, proxy);

if (contracts.length == 0) return new Decimal(0);
const costPromises = contracts.reduce((acc: Promise<Decimal>[], contract) => {
acc.push(this.calculateContractOverDue({ contractInfo: contract, gridProxyClient: proxy }));
return acc;
}, []);
const costResult = await Promise.all(costPromises);

const totalContractsCost = costResult.reduce((acc: Decimal, contractCost) => acc.add(contractCost), new Decimal(0));
return totalContractsCost;
}

/**
* 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;
if (contract.type == ContractType.Rent) {
const { cru, sru, mru, hru } = nodeDetails.total_resources;
/**node extra fees in mille USD per month */
const extraFeesInMilliUsd = await this.client.contracts.getDedicatedNodeExtraFee({ nodeId: nodeDetails.nodeId });
const extraFeeUSD = extraFeesInMilliUsd / 1000;
const USDCost = (
await calc.calculateWithMyBalance({
ipv4u: false,
certified: isCertified,
cru: cru,
mru: bytesToGB(mru),
hru: bytesToGB(hru),
sru: bytesToGB(sru),
})
).dedicatedPrice;

return USDCost + extraFeeUSD;
}

/** 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;
/** ip price in USD per hour */
const ipPrice = (await this.client.pricingPolicies.get({ id: 1 })).ipu.value / TFT_CONVERSION_FACTOR;
const pricePerMonth = ipPrice * HOURS_ONE_MONTH;
if (isCertified) return pricePerMonth * (125 / 100);
return pricePerMonth;
}

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: cru,
mru: bytesToGB(mru),
hru: bytesToGB(hru),
sru: bytesToGB(sru),
})
).sharedPrice;

return 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;

const { standardOverdraft, additionalOverdraft, lastUpdatedSeconds } =
await this.client.contracts.getContractPaymentState(contractInfo.contract_id);

/**Calculate the elapsed seconds since last billing*/
const elapsedSeconds = Math.ceil(Date.now() / 1000 - lastUpdatedSeconds);

// time since the last billing with allowance time of **one hour**
const totalPeriodTime = elapsedSeconds + SECONDS_ONE_HOUR;

/** Cost in USD */
const contractMonthlyCost = new Decimal(await this.getContractCost(contractInfo, options.gridProxyClient));

const contractMonthlyCostTFT = await this.convertToTFT(contractMonthlyCost);
/** contract cost per second in TFT */
const contractCostPerSecond = contractMonthlyCostTFT.div(HOURS_ONE_MONTH * SECONDS_ONE_HOUR);

/** cost of the current billing period and the mentioned allowance time in TFT*/
const totalPeriodCost = contractCostPerSecond.times(totalPeriodTime);

/**Calculate total overDraft in Unit TFT*/
const totalOverDraft = new Decimal(standardOverdraft).add(additionalOverdraft);

/** Un-billed amount in unit USD for the network usage, including the premium price for the certified node */
const unbilledNU = await this.getUnbilledNu(contractInfo.contract_id, contractInfo.details.nodeId);

const unbilledNuTFTUnit = await this.convertToTFT(new Decimal(unbilledNU));

const overdue = totalOverDraft.add(unbilledNuTFTUnit);

/** TFT */
const overdueTFT = overdue.div(TFT_CONVERSION_FACTOR);
const contractOverdue = overdueTFT.add(totalPeriodCost);

/** list all node contracts on the rented node and add their values */
if (contractInfo.type == ContractType.Rent) {
/** The contracts on the rented node, this includes total overdraft, total ips count, and total unbuilled amount*/
const totalContractsCost = await this.getContractsCostOnRentedNode(
contractInfo.details.nodeId,
options.gridProxyClient,
);
const totalContractsOverDue = contractOverdue.add(totalContractsCost);
return totalContractsOverDue;
}
return contractOverdue;
}

/**
* WARNING: Please be careful when executing this method, it will delete all your contracts.
* @param {CancelMyContractOptions} options
Expand Down Expand Up @@ -331,28 +603,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
Loading
Loading