diff --git a/packages/grid_client/src/clients/tf-grid/contracts.ts b/packages/grid_client/src/clients/tf-grid/contracts.ts index be5084abd1..7b55793679 100644 --- a/packages/grid_client/src/clients/tf-grid/contracts.ts +++ b/packages/grid_client/src/clients/tf-grid/contracts.ts @@ -107,6 +107,11 @@ export interface GetConsumptionOptions { graphqlURL: string; id: number; } +export interface GetConsumptionsOptions { + graphqlURL: string; + contractIds: number[]; + contractCreatedAt: Map; +} export interface Consumption { amountBilled: number; discountReceived: DiscountLevel; @@ -342,6 +347,95 @@ class TFContracts extends Contracts { } } + private calculateConsumption( + reports: GqlContractBillReports[], + contractId: number, + contractCreatedAt: Map, + ): Consumption { + if (reports.length === 0) { + return { amountBilled: 0, discountReceived: "None" }; + } + + let duration = 1; + const amountBilled = new Decimal(reports[0].amountBilled); + + if (reports.length === 2) { + duration = (reports[0].timestamp - reports[1].timestamp) / 3600; // one hour + } else { + const createdAt = contractCreatedAt.get(contractId); + if (createdAt) { + duration = (reports[0].timestamp - createdAt) / 3600; + } + } + + return { + amountBilled: amountBilled + .div(duration || 1) + .div(10 ** 7) + .toNumber(), + discountReceived: reports[0].discountReceived, + }; + } + + /** + * Get consumption details for multiple contracts at once. + * This reduces the number of GraphQL queries by fetching all billing reports in a single request. + * + * @param {GetConsumptionsOptions} options + * @returns {Promise>} A map of contract ID to consumption details + */ + async getConsumptions(options: GetConsumptionsOptions): Promise> { + if (options.contractIds.length === 0) { + return new Map(); + } + + const gqlClient = new Graphql(options.graphqlURL); + const contractIds = options.contractIds.map(id => id.toString()); + const whereClause = `{contractID_in: [${contractIds.join(", ")}]}`; + const body = `query getConsumptions { + contractBillReports(where: ${whereClause}, orderBy: timestamp_DESC) { + contractID + amountBilled + timestamp + discountReceived + } + }`; + + try { + const response = await gqlClient.query(body); + const billReports: GqlContractBillReports[] = ( + response["data"] as { contractBillReports: GqlContractBillReports[] } + ).contractBillReports; + + // Group reports by contract ID, keeping only the latest 2 per contract + const reportsByContract = new Map(); + for (const report of billReports) { + const contractId = +report.contractID; + const reports = reportsByContract.get(contractId) || []; + if (reports.length < 2) { + reports.push(report); + reportsByContract.set(contractId, reports); + } + } + + // Calculate consumption for each contract + const consumptions = new Map(); + for (const contractId of options.contractIds) { + const reports = reportsByContract.get(contractId) || []; + const consumption = this.calculateConsumption(reports, contractId, options.contractCreatedAt); + consumptions.set(contractId, consumption); + } + + return consumptions; + } catch (err) { + (err as Error).message = formatErrorMessage( + `Error getting consumptions for contracts [${options.contractIds.join(", ")}].`, + err, + ); + throw err; + } + } + async listContractsByAddress(options: ListContractByAddressOptions) { const twinId = await this.client.twins.getTwinIdByAccountId({ accountId: options.accountId }); return await this.listContractsByTwinId({ diff --git a/packages/grid_client/src/modules/contracts.ts b/packages/grid_client/src/modules/contracts.ts index 1a75795bd3..fe37773f87 100644 --- a/packages/grid_client/src/modules/contracts.ts +++ b/packages/grid_client/src/modules/contracts.ts @@ -33,6 +33,7 @@ import { BatchCancelContractsModel, ContractCancelModel, ContractConsumption, + ContractConsumptions, ContractDiscountPackage, ContractGetByNodeIdAndHashModel, ContractGetModel, @@ -555,6 +556,30 @@ class Contracts { return this.client.contracts.getConsumption({ id: options.id, graphqlURL: this.config.graphqlURL }); } + /** + * Get consumption details for multiple contracts at once. + * This reduces the number of GraphQL queries by fetching all billing reports in a single request. + * + * @param {ContractConsumptions} options - The options containing contract IDs and their creation timestamps + * @returns {Promise>} A map of contract ID to consumption details + * @decorators + * - `@expose`: Exposes the method for external use. + * - `@validateInput`: Validates the input options. + */ + @expose + @validateInput + async getConsumptions(options: ContractConsumptions): Promise> { + const contractCreatedAt = new Map(); + Object.entries(options.contractCreatedAt).forEach(([key, value]) => { + contractCreatedAt.set(+key, value); + }); + return this.client.contracts.getConsumptions({ + graphqlURL: this.config.graphqlURL, + contractIds: options.contractIds, + contractCreatedAt, + }); + } + /** * Retrieves the deletion time of a contract based on the provided options. * diff --git a/packages/grid_client/src/modules/models.ts b/packages/grid_client/src/modules/models.ts index 67b2e7b691..5e69973aa5 100644 --- a/packages/grid_client/src/modules/models.ts +++ b/packages/grid_client/src/modules/models.ts @@ -2,6 +2,7 @@ import { ExtrinsicResult } from "@threefold/tfchain_client"; import { default as AlgoSdk } from "algosdk"; import { Expose, Transform, Type } from "class-transformer"; import { + ArrayMinSize, ArrayNotEmpty, IsAlphanumeric, IsArray, @@ -12,6 +13,7 @@ import { IsIP, IsNotEmpty, IsNumber, + IsObject, IsOptional, IsString, IsUrl, @@ -390,6 +392,11 @@ class ContractConsumption { @Expose() @IsInt() @Min(1) id: number; } +class ContractConsumptions { + @Expose() @IsArray() @ArrayMinSize(0) contractIds: number[]; + @Expose() @IsObject() contractCreatedAt: Record; +} + class ContractDiscountPackage { @Expose() @IsInt() @Min(1) id: number; } @@ -988,6 +995,7 @@ export { ContractsByTwinId, ContractsByAddress, ContractConsumption, + ContractConsumptions, ContractDiscountPackage, ContractLockModel, TwinCreateModel, diff --git a/packages/playground/src/utils/contracts.ts b/packages/playground/src/utils/contracts.ts index a4e473bf26..eab3c6751d 100644 --- a/packages/playground/src/utils/contracts.ts +++ b/packages/playground/src/utils/contracts.ts @@ -36,6 +36,7 @@ export async function normalizeContract( grid: GridClient, c: { [key: string]: any }, type: ContractType, + consumption?: Consumption, ): Promise { const id = +c.contract_id; @@ -53,11 +54,15 @@ export async function normalizeContract( expiration = new Date(exp).toLocaleString(); } - let consumption: Consumption; - try { - consumption = await grid.contracts.getConsumption({ id }); - } catch { - consumption = { amountBilled: 0, discountReceived: "None" }; + let contractConsumption: Consumption; + if (consumption) { + contractConsumption = consumption; + } else { + try { + contractConsumption = await grid.contracts.getConsumption({ id }); + } catch { + contractConsumption = { amountBilled: 0, discountReceived: "None" }; + } } return { @@ -75,8 +80,8 @@ export async function normalizeContract( solutionName: data.name || "-", solutionType: data.projectName || data.type || "-", expiration, - consumption: consumption.amountBilled, - discountPackage: consumption.discountReceived, + consumption: contractConsumption.amountBilled, + discountPackage: contractConsumption.discountReceived, }; } diff --git a/packages/playground/src/weblets/tf_contracts_list.vue b/packages/playground/src/weblets/tf_contracts_list.vue index a6c5fa57d9..1dac0acda6 100644 --- a/packages/playground/src/weblets/tf_contracts_list.vue +++ b/packages/playground/src/weblets/tf_contracts_list.vue @@ -60,7 +60,7 @@ :disabled="loadingContracts || loadingTotalCost" @click=" contractsTable.forEach(t => t.reset()); - loadContracts(); + loadContracts(undefined, undefined, true); " > refresh @@ -253,7 +253,7 @@