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 compatibility #3345

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions packages/grid_client/src/clients/tf-grid/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Contract,
ContractLock,
ContractLockOptions,
ContractPaymentState,
Contracts,
ExtrinsicResult,
GetDedicatedNodePriceOptions,
Expand All @@ -13,6 +14,8 @@ import { formatErrorMessage } from "../../helpers";
import { ContractStates } from "../../modules";
import { Graphql } from "../graphql/client";

const ONE_HOUR = 1000 * 60 * 60;
Copy link
Member

Choose a reason for hiding this comment

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

In what unit exactly? Please change the variable name to a more descriptive one.
An ideal name for a variable that holds the number of milliseconds in one hour could be MILLISECONDS_PER_HOUR, which is descriptive and clearly convey the purpose of the variable.


export type DiscountLevel = "None" | "Default" | "Bronze" | "Silver" | "Gold";

export interface ListContractByTwinIdOptions {
Expand Down Expand Up @@ -98,6 +101,11 @@ export interface GetConsumptionOptions {
id: number;
}

export interface CalculateOverdueOptions {
graphqlURL: string;
id: number;
paymentState: ContractPaymentState;
}
export interface CancelMyContractOptions {
graphqlURL: string;
}
Expand All @@ -109,6 +117,18 @@ export interface LockContracts {
rentContracts: LockDetails;
totalAmountLocked: number;
}
export interface ContractOverdueDetails extends ContractPaymentState {
overdueAmount: number;
}

export type OverdueDetails = { [key: number]: ContractOverdueDetails };

export interface ContractsOverdue {
nameContracts: OverdueDetails;
nodeContracts: OverdueDetails;
rentContracts: OverdueDetails;
totalOverdueAmount: number;
}

class TFContracts extends Contracts {
async listContractsByTwinId(options: ListContractByTwinIdOptions): Promise<GqlContracts> {
Expand Down Expand Up @@ -308,6 +328,30 @@ class TFContracts extends Contracts {
return res;
}

/**
* Calculates the overdue amount for a contract.
*
* This method calculates the overdue amount by summing the overdraft amounts
* from the contract payment state and multiplying it by the product of:
* 1. The time elapsed since the last billing in seconds (with an additional time allowance).
* 2. The contract cost per second.
*
* The resulting overdue amount represents the amount that needs to be addressed.
*
* @param {CalculateOverdueOptions} options - The options containing the contract ID.
* @returns {Promise<number>} - The calculated overdue amount.
*/
async calculateContractOverDue(options: CalculateOverdueOptions) {
const { lastUpdatedSeconds, standardOverdraft, additionalOverdraft } = options.paymentState;
const contractCost = await this.getConsumption({ id: options.id, graphqlURL: options.graphqlURL });
Copy link
Member

@sameh-farouk sameh-farouk Sep 2, 2024

Choose a reason for hiding this comment

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

This is just an estimate, as some contracts might have public IP addresses, and the cost will depend on the amount of bandwidth used, which is likely to vary.

The cost calculation should simulate the billing process on tfchain (dry run) to provide a more accurate estimate. This involves checking the pricing policy, NU, and other resource consumption to determine the actual cost of the contract. Then, account for the time between the calculation and the submission of the transaction.

We still need to estimate the cost of this extra time, but it shouldn't be a problem. Since the cost changes dynamically, we should also update the cost accordingly. It's unlikely that we will receive a new NU report, especially if we refresh the cost at short intervals, such as every 10~20 seconds.

const elapsedSeconds = new Decimal(Date.now()).minus(lastUpdatedSeconds).div(1000);
Copy link
Member

@sameh-farouk sameh-farouk Sep 2, 2024

Choose a reason for hiding this comment

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

Given that ContractPaymentState.lastUpdatedSecond is already a timestamp in seconds, you need to convert the current timestamp from milliseconds to seconds before performing the subtraction, right?

The corrected code would be

const elapsedSeconds = new Decimal(Date.now()).div(1000).minus(lastUpdatedSeconds);

Date.now() returns the current timestamp in milliseconds, then .div(1000) converts the current time from milliseconds to seconds by dividing by 1000. finally .minus(lastUpdatedSeconds) subtracts lastUpdatedSeconds (which is already in seconds) from the current time in seconds.

const overdue = standardOverdraft
Copy link
Member

@sameh-farouk sameh-farouk Sep 2, 2024

Choose a reason for hiding this comment

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

Can you please explain how the overdue is calculated, add a comment, and possibly simplify it by calculating it progressively using descriptive intermediary variable names?

.plus(additionalOverdraft)
.times(elapsedSeconds.plus(ONE_HOUR).times((contractCost / 60) * 60));
Copy link
Member

@sameh-farouk sameh-farouk Sep 2, 2024

Choose a reason for hiding this comment

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

Units are not consistent here.
From what I see elapsedSeconds is in seconds, while ONE_HOUR is in milliseconds.
Perhaps you intended to set ONE_HOUR to 3600 (seconds) instead of 3600000 (milliseconds)?

Also, I'm trying to understand why we divide by 60 and then multiply by 60.


return Number(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.

Again, add a comment to explain the division here, and/or first assign the result to a descriptive variable name like overdueTFT for better reliability, then return it. you can refer to the raw amount (before division) as overdueMicroTFT

}

/**
* WARNING: Please be careful when executing this method, it will delete all your contracts.
* @param {CancelMyContractOptions} options
Expand Down
70 changes: 70 additions & 0 deletions packages/grid_client/src/modules/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { DeploymentKeyDeletionError, InsufficientBalanceError } from "@threefold
import * as PATH from "path";

import {
ContractOverdueDetails,
ContractsOverdue,
GqlContracts,
GqlNameContract,
GqlNodeContract,
Expand All @@ -25,6 +27,7 @@ import {
ContractGetByNodeIdAndHashModel,
ContractGetModel,
ContractLockModel,
ContractOverdueModel,
ContractState,
ContractStates,
CreateServiceContractModel,
Expand Down Expand Up @@ -603,6 +606,73 @@ class Contracts {
async unlockMyContracts(): Promise<number[]> {
return await this.client.contracts.unlockMyContracts(this.config.graphqlURL);
}

/**
* Retrieves the `payment state` of a contract based on the provided `contract ID`.
*
* @param contractID - The ID for which contract to retrieve its `payment state`.
* @returns {Promise<ContractLock>} A Promise that resolves to the `payment state` of the specified contract.
*/
async getContractPaymentState(contractID: number) {
return await this.client.contracts.getContractPaymentState(contractID);
}

/**
* Returns the contract payment state with its overdue amount of the contract.
*
* @param {ContractOverdueModel} options - Options to get the amount.
* @returns {Promise<ContractOverdueDetails>} A promise that resolves contract overdue details.
* @decorators
* - `@expose`: Exposes the method for external use.
* - `@validateInput`: Validates the input options.
*/
@expose
@validateInput
async getContractOverdueDetails(options: ContractOverdueModel): Promise<ContractOverdueDetails> {
const paymentState = await this.getContractPaymentState(options.id);
const overdueAmount = await this.client.contracts.calculateContractOverDue({
id: options.id,
graphqlURL: this.config.graphqlURL,
paymentState,
});
return {
...paymentState,
overdueAmount,
};
}

/**
* Retrieves overdue details of contracts.
* @returns {Promise<ContractsOverdue>} A Promise that resolves to an object of type ContractsOverdue containing details of locked contracts.
* @decorators
* - `@expose`: Exposes the method for external use.
* - `@validateInput`: Validates the input options.
*/
@expose
@validateInput
async getContractsOverdueAmount(): Promise<ContractsOverdue> {
const contractsOverdue = {
nameContracts: {},
nodeContracts: {},
rentContracts: {},
totalOverdueAmount: 0,
};
const contracts = await this.listMyContracts({ state: [ContractStates.GracePeriod] });

if (contracts == undefined) return contractsOverdue;

const contractTypes = ["nameContracts", "nodeContracts", "rentContracts"];

for (const type of contractTypes) {
for (const contract of contracts[type]) {
const contractID = parseInt(contract.contractID);
const contractOverdueDetails = await this.getContractOverdueDetails({ id: contractID });
contractsOverdue[type][contractID] = contractOverdueDetails;
contractsOverdue.totalOverdueAmount += contractOverdueDetails.overdueAmount;
}
}
return contractsOverdue;
}
}

export { Contracts as contracts };
2 changes: 2 additions & 0 deletions packages/grid_client/src/modules/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ class ContractConsumption {
}

class ContractLockModel extends ContractConsumption {}
class ContractOverdueModel extends ContractConsumption {}

class TwinCreateModel {
@Expose() @IsString() @IsNotEmpty() relay: string;
Expand Down Expand Up @@ -971,6 +972,7 @@ export {
ContractsByAddress,
ContractConsumption,
ContractLockModel,
ContractOverdueModel,
TwinCreateModel,
TwinGetModel,
TwinGetByAccountIdModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export default {
ipv4u: resources.value.ipv4,
certified: resources.value.certified,
balance:
//TODO check if we need to use the total balance
userBalance.value && resources.value.useCurrentBalance ? userBalance.value.free : +resources.value.balance,
nu: +resources.value.nu,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
TFTs.
</p>
<v-alert
v-if="contractLocked?.amountLocked == 0 && isNodeInRentContracts"
v-if="contractLocked?.overdueAmount == 0 && isNodeInRentContracts"
class="my-4"
type="warning"
variant="tonal"
Expand Down Expand Up @@ -278,9 +278,13 @@

<script lang="ts" setup>
// Import necessary types and libraries
import { ContractStates, type GridClient, type LockDetails } from "@threefold/grid_client";
import {
type ContractOverdueDetails,
ContractStates,
type GridClient,
type OverdueDetails,
} from "@threefold/grid_client";
import type { NodeStatus } from "@threefold/gridproxy_client";
import type { ContractLock } from "@threefold/tfchain_client";
import { TFChainError } from "@threefold/tfchain_client";
import { DeploymentKeyDeletionError } from "@threefold/types";
import { capitalize, computed, defineComponent, type PropType, type Ref, ref, watch } from "vue";
Expand Down Expand Up @@ -318,7 +322,7 @@ const props = defineProps({
required: true,
},
lockedContracts: {
type: Object as PropType<LockDetails>,
type: Object as PropType<OverdueDetails>,
required: true,
},
size: {
Expand All @@ -335,7 +339,7 @@ const props = defineProps({
},
});
const getAmountLocked = computed(() => {
const amountLocked = contractLocked?.value?.amountLocked ?? 0;
const amountLocked = contractLocked?.value?.overdueAmount ?? 0;
return amountLocked > 0 ? parseFloat(amountLocked.toFixed(3)) : 0;
});

Expand All @@ -344,7 +348,7 @@ const isNodeInRentContracts = computed(() => {
const nodeIds = new Set(
props.contracts.value.map(contract => contract.details.nodeId).filter(nodeId => nodeId !== undefined) as number[],
);
if (contractLocked.value && contractLocked.value.amountLocked === 0) {
if (contractLocked.value && contractLocked.value.overdueAmount === 0) {
return nodeIds.has(selectedItem.value.details.nodeId);
}
}
Expand Down Expand Up @@ -372,7 +376,7 @@ function updateSortBy(sort: { key: string; order: "asc" | "desc" }[]) {
}

const layout = ref();
const contractLocked = ref<ContractLock>();
const contractLocked = ref<ContractOverdueDetails>();
const deleting = ref<boolean>(false);
const loadingShowDetails = ref<boolean>(false);
const contractStateDialog = ref<boolean>(false);
Expand All @@ -384,7 +388,7 @@ const selectedContracts = ref<NormalizedContract[]>([]);
const selectedItem = ref();
const profileManagerController = useProfileManagerController();
const balance = profileManagerController.balance;
const freeBalance = computed(() => (balance.value?.free ?? 0) - (balance.value?.locked ?? 0));
const freeBalance = computed(() => balance.value?.free ?? 0);
const unlockContractLoading = ref(false);
const unlockDialog = ref(false);
const selectedLockedContracts = computed(() => {
Expand Down Expand Up @@ -431,14 +435,14 @@ async function openUnlockDialog() {
if (contract.consumption == 0 && contract.type == ContractType.Node && contract.details.nodeId) {
nodeIDsGracePeriod.add(contract.details.nodeId);
} else {
selectedLockedAmount.value += (await getLockDetails(contract.contract_id)).amountLocked || 0;
selectedLockedAmount.value += (await getLockDetails(contract.contract_id)).overdueAmount || 0;
}
}
for (const nodeId of nodeIDsGracePeriod) {
const rentContractId = await props.grid.nodes.getRentContractId({ nodeId });
if (rentContractId) {
rentContractIds.value.push(rentContractId);
selectedLockedAmount.value += (await getLockDetails(rentContractId)).amountLocked || 0;
selectedLockedAmount.value += (await getLockDetails(rentContractId)).overdueAmount || 0;
}
}
await profileManagerController.reloadBalance();
Expand All @@ -451,18 +455,16 @@ async function openUnlockDialog() {
}
}

async function getLockDetails(contractId: number) {
return await props.grid.contracts.contractLock({ id: contractId });
async function getLockDetails(contractId: number): Promise<ContractOverdueDetails> {
return await props.grid.contracts.getContractOverdueDetails({ id: contractId });
}
// Function to fetch contract lock details
async function contractLockDetails(item: any) {
selectedItem.value = item;
loadingShowDetails.value = true;
await profileManagerController.reloadBalance();
await getLockDetails(item.contract_id);
await props.grid?.contracts
.contractLock({ id: item.contract_id })
.then((data: ContractLock) => {
await getLockDetails(item.contract_id)
.then((data: ContractOverdueDetails) => {
contractLocked.value = data;
contractStateDialog.value = true;
})
Expand Down Expand Up @@ -520,7 +522,7 @@ async function onDelete() {
async function unlockContract(contractId: number[]) {
try {
unlockContractLoading.value = true;
await props.grid.contracts.unlockContracts(contractId.filter(id => props.lockedContracts[id]?.amountLocked !== 0));
await props.grid.contracts.unlockContracts(contractId.filter(id => props.lockedContracts[id]?.overdueAmount !== 0));
createCustomToast(
`Your request to unlock contract ${contractId} has been processed successfully. Changes may take a few minutes to reflect`,
ToastType.info,
Expand Down
8 changes: 4 additions & 4 deletions packages/playground/src/components/weblet_layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,10 @@ defineExpose({
message.value = "Checking your balance...";

const balance = await loadBalance(grid);
const b = balance.free - balance.locked;

if (b < min) {
throw new Error(`You have ${b.toFixed(2)} TFT but it's required to have at least ${min} TFT.`);
if (balance.free < min) {
throw new Error(
`You have ${balance.free.toFixed(2)} TFT available, but it's required to have at least ${min} TFT.`,
);
}
message.value = "You have enough TFT to continue...";
return balance;
Expand Down
5 changes: 3 additions & 2 deletions packages/playground/src/utils/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,14 @@ export function activateAccountAndCreateTwin(mnemonic: string) {

export interface Balance {
free: number;
locked: number;
reserved: number;
}
export async function loadBalance(grid: GridClient): Promise<Balance> {
const balance = await grid.balance.getMyBalance();
return {
//TODO should we add a field for total; instead of keep calculate it ?
free: +balance.free,
locked: +balance.frozen,
reserved: +balance.reserved,
};
}

Expand Down
9 changes: 4 additions & 5 deletions packages/playground/src/weblets/profile_manager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
<p>
Balance:
<strong :class="theme.name.value === AppThemeSelection.light ? 'text-primary' : 'text-info'">
{{ normalizeBalance(balance.free, true) }} TFT
{{ normalizeBalance(balance.free + balance.reserved, true) }} TFT
</strong>
</p>
<p>
Locked:
<strong :class="theme.name.value === AppThemeSelection.light ? 'text-primary' : 'text-info'">
{{ normalizeBalance(balance.locked, true) || 0 }} TFT
{{ normalizeBalance(balance.reserved, true) || 0 }} TFT
</strong>
<v-tooltip text="Locked balance documentation" location="bottom right">
<template #activator="{ props }">
Expand Down Expand Up @@ -619,7 +619,7 @@ const isValidConnectConfirmationPassword = computed(() =>
const profileManagerController = useProfileManagerController();

const balance = profileManagerController.balance;
let freeBalance = balance.value?.free ?? 0;
const freeBalance = computed(() => balance.value?.free ?? 0);

const email = ref("");

Expand Down Expand Up @@ -773,8 +773,7 @@ async function __loadBalance(profile?: Profile, tries = 1) {
loadingBalance.value = true;
const grid = await getGrid(profile);
balance.value = await loadBalance(grid!);
freeBalance = balance.value.free ?? 0;
if (!BalanceWarningRaised && balance.value?.free) {
if (!BalanceWarningRaised && freeBalance.value) {
if (balance.value?.free < 0.01) {
createCustomToast("Your balance is too low, Please fund your account.", ToastType.warning);
BalanceWarningRaised = true;
Expand Down
Loading
Loading