diff --git a/packages/grid_client/scripts/compare_locked_balance.ts b/packages/grid_client/scripts/compare_locked_balance.ts index da915947af..bbb7d0b85e 100644 --- a/packages/grid_client/scripts/compare_locked_balance.ts +++ b/packages/grid_client/scripts/compare_locked_balance.ts @@ -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 })), diff --git a/packages/grid_client/src/clients/tf-grid/contracts.ts b/packages/grid_client/src/clients/tf-grid/contracts.ts index 903c17786c..122124a64d 100644 --- a/packages/grid_client/src/clients/tf-grid/contracts.ts +++ b/packages/grid_client/src/clients/tf-grid/contracts.ts @@ -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"; @@ -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 { options.stateList = options.stateList || [ContractStates.Created, ContractStates.GracePeriod]; @@ -297,6 +341,7 @@ class TFContracts extends Contracts { }); } + /** @deprecated */ async contractLock(options: ContractLockOptions) { const res = await super.contractLock(options); const amountLocked = new Decimal(res.amountLocked); @@ -304,6 +349,233 @@ class TFContracts extends Contracts { 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} queries + * @returns + */ + async listAllContracts(proxy: GridProxyClient, queries: Partial) { + 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 { + return await this.listAllContracts(proxy, { + nodeId, + state: [ContractState.GracePeriod], + numberOfPublicIps: 1, + }); + } + + /** + * 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 { + const contracts = await this.getNodeContractsOnRentedNode(nodeId, proxy); + + if (contracts.length == 0) return new Decimal(0); + const costPromises = contracts.reduce((acc: Promise[], 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} - 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 @@ -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` 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 = 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[] = []; - 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 { diff --git a/packages/grid_client/src/helpers/utils.ts b/packages/grid_client/src/helpers/utils.ts index d8aac6702c..60ab9d0d6e 100644 --- a/packages/grid_client/src/helpers/utils.ts +++ b/packages/grid_client/src/helpers/utils.ts @@ -81,6 +81,14 @@ function convertObjectToQueryString(obj: Record): 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); +} /** * Converts a string message to its hexadecimal representation. * @param message - The message to convert. @@ -111,6 +119,7 @@ export { generateRandomHexSeed, zeroPadding, convertObjectToQueryString, + bytesToGB, stringToHex, bytesFromHex, }; diff --git a/packages/grid_client/src/modules/calculator.ts b/packages/grid_client/src/modules/calculator.ts index 3ee9f0ba56..7ad86ef682 100644 --- a/packages/grid_client/src/modules/calculator.ts +++ b/packages/grid_client/src/modules/calculator.ts @@ -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} - 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. * diff --git a/packages/grid_client/src/modules/contracts.ts b/packages/grid_client/src/modules/contracts.ts index 571d8d456c..c92c01b444 100644 --- a/packages/grid_client/src/modules/contracts.ts +++ b/packages/grid_client/src/modules/contracts.ts @@ -1,8 +1,16 @@ +import GridProxyClient, { + Contract as GridProxyContract, + ContractState as GridProxyContractState, + ContractType, + Pagination, +} from "@threefold/gridproxy_client"; import { Contract, ContractLock, ServiceContract } from "@threefold/tfchain_client"; import { DeploymentKeyDeletionError, InsufficientBalanceError } from "@threefold/types"; +import { GridClientError } from "@threefold/types"; import * as PATH from "path"; import { + ContractsOverdue, GqlContracts, GqlNameContract, GqlNodeContract, @@ -13,6 +21,7 @@ import { } from "../clients/tf-grid"; import { TFClient } from "../clients/tf-grid/client"; import { GridClientConfig } from "../config"; +import { formatErrorMessage } from "../helpers"; import { events } from "../helpers/events"; import { expose } from "../helpers/expose"; import { validateInput } from "../helpers/validator"; @@ -235,7 +244,7 @@ class Contracts { /** * Returns the lock details of the contract. - * + * @deprecated * @param {ContractLockModel} options - The options for locking the contract. * @returns {Promise} A promise that resolves when the contract is successfully locked. * @decorators @@ -547,6 +556,7 @@ class Contracts { /** * Retrieves lock details of contracts. + * @deprecated * @returns {Promise} A Promise that resolves to an object of type LockContracts containing details of locked contracts. * @decorators * - `@expose`: Exposes the method for external use. @@ -578,8 +588,146 @@ class Contracts { return LockedContracts; } + /** + * Retrieves the overdue amount for a given contract . + * It is a private function that helps other get contracts overdue amount. + * + * @param {GridProxyContract} contract - The contract for which the overdue amount is to be calculated. + * @param {GridProxyClient} proxy - The proxy client will be used overdue calculation. + * + * @returns {Promise} - A promise that resolves to the overdue amount for the specified contract. + */ + @validateInput + private async getContractOverdueAmount(contract: GridProxyContract, proxy: GridProxyClient) { + return await this.client.contracts.calculateContractOverDue({ contractInfo: contract, gridProxyClient: proxy }); + } + + /** + * Retrieves the overdue amount for a given contract. + * + * @param {GridProxyContract} contract - The contract for which the overdue amount is to be calculated. + * + * @returns {Promise} - A promise that resolves to the overdue amount for the specified contract. + */ + @expose + @validateInput + async getContractOverdueAmountByContract(contract: GridProxyContract) { + try { + const proxy = new GridProxyClient(this.config.proxyURL); + return await this.client.contracts.calculateContractOverDue({ contractInfo: contract, gridProxyClient: proxy }); + } catch (error) { + console.log(error); + + (error as Error).message = formatErrorMessage("Failed to get contract overdue info", error); + throw error; + } + } + + /** + * Retrieves the overdue amount for a given contract by its id. + * + * @param {number} contractId - The contract id for which contract the overdue amount is to be calculated. + * + * @returns {Promise} - A promise that resolves to the overdue amount for the specified contract. + */ + @expose + @validateInput + async getContractOverdueAmountById(contractId: number) { + try { + const proxy = new GridProxyClient(this.config.proxyURL); + const contract = (await proxy.contracts.list({ contractId })).data[0]; + if (!contract) throw new GridClientError(`Can't find contract ${contractId} info`); + return await this.getContractOverdueAmount(contract, proxy); + } catch (error) { + (error as Error).message = formatErrorMessage("Failed to get contract overdue info", error); + throw error; + } + } + + /** + * Retrieves the overdue amount for all grace period contracts for the current user. + * + * @returns {Promise} - A promise that resolves to the overdue amount for the specified contract. + */ + @expose + async getTotalOverdue() { + const proxy = new GridProxyClient(this.config.proxyURL); + const result: ContractsOverdue = { + nameContracts: {}, + nodeContracts: {}, + rentContracts: {}, + totalOverdueAmount: 0, + }; + + const contracts = await proxy.contracts.list({ + twinId: this.config.twinId, + state: [GridProxyContractState.GracePeriod], + }); + const rentedNodesIds: number[] = []; + for (const contract of contracts.data) { + switch (contract.type) { + case ContractType.Name: + result.nameContracts[contract.contract_id] = ( + await this.getContractOverdueAmount(contract, proxy) + ).toNumber(); + result.totalOverdueAmount += result.nameContracts[contract.contract_id]; + break; + + case ContractType.Rent: + result.rentContracts[contract.contract_id] = ( + await this.getContractOverdueAmount(contract, proxy) + ).toNumber(); + rentedNodesIds.push(contract.details.nodeId); + result.totalOverdueAmount += result.rentContracts[contract.contract_id]; + break; + + default: + /** skip node contracts on rented nodes as it already calculated in the rent contract */ + if (rentedNodesIds.includes(contract.details.nodeId)) { + result.nodeContracts[contract.contract_id] = 0; + } else { + result.nodeContracts[contract.contract_id] = ( + await this.getContractOverdueAmount(contract, proxy) + ).toNumber(); + result.totalOverdueAmount += result.nodeContracts[contract.contract_id]; + } + } + } + return result; + } + /** * Unlocks multiple contracts. + * @param contracts An array of contracts to be unlocked. + * @returns A Promise that resolves to an array of billed contracts representing the result of batch unlocking. + * @decorators + * - `@expose`: Exposes the method for external use. + * - `@validateInput`: Validates the input options. + */ + @expose + @validateInput + async unlockContracts(contracts: GridProxyContract[]) { + const proxy = new GridProxyClient(this.config.proxyURL); + return await this.batchUnlockContracts(contracts, proxy); + } + + /** + * Unlocks multiple contracts. + * @param contracts An array of contracts to be unlocked. + * @param proxy An instance of Grid proxy client. + * @returns A Promise that resolves to an array of billed contracts representing the result of batch unlocking. + * @decorators + * - `@expose`: Exposes the method for external use. + * - `@validateInput`: Validates the input options. + */ + @expose + @validateInput + async batchUnlockContracts(contracts: GridProxyContract[], proxy: GridProxyClient) { + return await this.client.contracts.batchUnlockContracts(contracts, proxy); + } + + /** + * Unlocks multiple contracts by their ids. * @param ids An array of contract IDs to be unlocked. * @returns A Promise that resolves to an array of billed contracts representing the result of batch unlocking. * @decorators @@ -588,9 +736,20 @@ class Contracts { */ @expose @validateInput - async unlockContracts(ids: number[]): Promise { - return await this.client.contracts.batchUnlockContracts(ids); + async unlockContractsByIds(ids: number[]): Promise { + try { + const proxy = new GridProxyClient(this.config.proxyURL); + const promises: Promise>[] = []; + ids.forEach(id => promises.push(proxy.contracts.list({ contractId: id }))); + const res = await Promise.all(promises); + const contracts = res.map(contract => contract.data[0]).filter(contract => contract != undefined); + return await this.batchUnlockContracts(contracts, proxy); + } catch (error) { + (error as Error).message = formatErrorMessage("Failed to get the contracts due:", error); + throw error; + } } + /** * Unlocks contracts associated with the current user. * @returns A Promise that resolves to an array of billed contracts. @@ -601,7 +760,7 @@ class Contracts { @expose @validateInput async unlockMyContracts(): Promise { - return await this.client.contracts.unlockMyContracts(this.config.graphqlURL); + return await this.client.contracts.unlockMyContracts(this.config.proxyURL); } } diff --git a/packages/grid_client/tests/README.md b/packages/grid_client/tests/README.md index d9fa640695..149bcaa896 100644 --- a/packages/grid_client/tests/README.md +++ b/packages/grid_client/tests/README.md @@ -42,5 +42,5 @@ yarn test --runInBand > `--coverage`: Collect and report coverage. > `--colors`: Force colorful output. > `--forceExit`: Force the process to exit after tests complete. -> +> > **Note:** Some tests are using Mycelium for the SSH connection, which is why it is important for Mycelium to be running. diff --git a/packages/playground/src/components/applications/ApplicationCards.vue b/packages/playground/src/components/applications/ApplicationCards.vue index 03c6295847..63edd33855 100644 --- a/packages/playground/src/components/applications/ApplicationCards.vue +++ b/packages/playground/src/components/applications/ApplicationCards.vue @@ -7,7 +7,7 @@ @@ -144,25 +148,20 @@ Contract lock Details -

+

Amount Locked: {{ getAmountLocked }} TFTs.

- - This contract is in a grace period because it's on a dedicated machine also in a grace period. That's why this - node is locked with zero TFTS and no billing rate. + + This contract is in a grace period because it's on a dedicated machine also in a grace period. Check its rent + contract {{ rentContracts[selectedItem.details.nodeId] }} The Contracts in Grace Period, which means that your workloads are suspended but not deleted; in order to - resume your workloads and restore their functionality, Please fund your account with the amount mentioned - above.
@@ -180,7 +179,7 @@
Unlock the following Contracts - + -
Loading contracts lock details
+
Loading contracts lock details
-
+
You have enough balance to unlock your contracts; this will cost you around {{ Math.ceil(selectedLockedAmount) }} TFTs.
- You need to fund your account with - {{ Math.ceil(selectedLockedAmount - freeBalance) }} TFTs - to resume your contracts +
+ Please fund your account with + {{ Math.ceil(selectedLockedAmount - freeBalance) }} TFTs + to resume {{ selectedContracts.length > 1 ? "those contracts" : "this contract" }}. +
+
+ Note that this amount will allow you to resume the contracts for up to one hour only. Make sure to + complete the funding promptly to avoid any interruptions! +
{{ c.contract_id }} + + + + @@ -258,13 +271,11 @@