From cf84039a9e5a87415cb59666dd9b8446a95fa8fd Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Mon, 17 Nov 2025 11:58:10 +0200 Subject: [PATCH 1/3] Implement getTFTBillingRate in tfchain client & replace get tft price w getTFTBillingRate in grid client & playground --- packages/grid_client/scripts/currency.ts | 2 +- .../src/clients/tf-grid/contracts.ts | 2 +- .../src/clients/tf-grid/tftPrice.ts | 5 ++++ .../grid_client/src/modules/calculator.ts | 2 +- .../playground/src/components/TfSwapPrice.vue | 2 +- .../src/weblets/tf_contracts_list.vue | 2 +- packages/tfchain_client/src/tft_price.ts | 30 +++++++++++++++++++ 7 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/grid_client/scripts/currency.ts b/packages/grid_client/scripts/currency.ts index 408836fbd0..e39c6bc306 100644 --- a/packages/grid_client/scripts/currency.ts +++ b/packages/grid_client/scripts/currency.ts @@ -62,7 +62,7 @@ function yearlyUSD(hourlyUSD) { } async function main() { const grid = await getClient(); - const rate = await grid.tfclient.tftPrice.get(); + const rate = await grid.tfclient.tftPrice.getTFTBillingRate(); const decimals = 3; currency = new TFTUSDConversionService(rate, decimals); diff --git a/packages/grid_client/src/clients/tf-grid/contracts.ts b/packages/grid_client/src/clients/tf-grid/contracts.ts index be5084abd1..03eaa1e628 100644 --- a/packages/grid_client/src/clients/tf-grid/contracts.ts +++ b/packages/grid_client/src/clients/tf-grid/contracts.ts @@ -387,7 +387,7 @@ class TFContracts extends Contracts { */ private async convertToTFT(USD: Decimal) { try { - const tftPrice = (await this.client.tftPrice.get()) ?? 0; + const tftPrice = (await this.client.tftPrice.getTFTBillingRate()) ?? 0; const tft = new currency(tftPrice, 15).convertUSDtoTFT({ amount: USD.toNumber() }); return new Decimal(tft); } catch (error) { diff --git a/packages/grid_client/src/clients/tf-grid/tftPrice.ts b/packages/grid_client/src/clients/tf-grid/tftPrice.ts index 9c19668fbe..9072cbe4d6 100644 --- a/packages/grid_client/src/clients/tf-grid/tftPrice.ts +++ b/packages/grid_client/src/clients/tf-grid/tftPrice.ts @@ -5,6 +5,11 @@ class TFTPrice extends QueryTFTPrice { const priceInMili = await super.get(); return priceInMili / 1000; } + + async getTFTBillingRate(): Promise { + const priceInMili = await super.getTFTBillingRate(); + return priceInMili / 1000; + } } export { TFTPrice }; diff --git a/packages/grid_client/src/modules/calculator.ts b/packages/grid_client/src/modules/calculator.ts index 7ad86ef682..986a23fc99 100644 --- a/packages/grid_client/src/modules/calculator.ts +++ b/packages/grid_client/src/modules/calculator.ts @@ -149,7 +149,7 @@ class Calculator { @expose @validateInput async tftPrice(): Promise { - const pricing = await this.client.tftPrice.get(); + const pricing = await this.client.tftPrice.getTFTBillingRate(); return this.client instanceof TFClient ? pricing : pricing / 1000; } diff --git a/packages/playground/src/components/TfSwapPrice.vue b/packages/playground/src/components/TfSwapPrice.vue index ccbed90200..4a299addd5 100644 --- a/packages/playground/src/components/TfSwapPrice.vue +++ b/packages/playground/src/components/TfSwapPrice.vue @@ -48,7 +48,7 @@ export default { setup() { const priceTask = useAsync( async () => { - const [price, error] = await resolveAsync(queryClient.tftPrice.get()); + const [price, error] = await resolveAsync(queryClient.tftPrice.getTFTBillingRate()); if (!price || error) { throw `Failed to load TFT price. Please click to reload.`; } diff --git a/packages/playground/src/weblets/tf_contracts_list.vue b/packages/playground/src/weblets/tf_contracts_list.vue index a6c5fa57d9..1c61d9f348 100644 --- a/packages/playground/src/weblets/tf_contracts_list.vue +++ b/packages/playground/src/weblets/tf_contracts_list.vue @@ -514,7 +514,7 @@ async function getTotalCost() { loadingTotalCost.value = true; const res = await gridProxyClient.twins.getConsumption(profileManager.profile!.twinId); totalTFT.value = +(res.last_hour_consumption || 0).toFixed(3); - const tftPrice = await queryClient.tftPrice.get(); + const tftPrice = await queryClient.tftPrice.getTFTBillingRate(); totalCostUSD.value = +(totalTFT.value * (tftPrice / 1000)).toFixed(3); } catch (error: any) { totalTFT.value = 0; diff --git a/packages/tfchain_client/src/tft_price.ts b/packages/tfchain_client/src/tft_price.ts index c86899a489..72f56041d2 100644 --- a/packages/tfchain_client/src/tft_price.ts +++ b/packages/tfchain_client/src/tft_price.ts @@ -11,6 +11,36 @@ class QueryTFTPrice { const res = await this.client.api.query.tftPriceModule.tftPrice(); return res.toPrimitive() as number; } + + @checkConnection + async getTFTBillingRate(): Promise { + const [minRes, maxRes, avgRes] = await Promise.all([ + this.client.api.query.tftPriceModule.minTftPrice(), + this.client.api.query.tftPriceModule.maxTftPrice(), + this.client.api.query.tftPriceModule.averageTftPrice(), + ]); + + const minPrice = (minRes.toPrimitive() as number) ?? 0; + const maxPrice = (maxRes.toPrimitive() as number) ?? 0; + const averagePrice = (avgRes.toPrimitive() as number) ?? 0; + + // Clamp the average between the configured bounds, matching on-chain billing logic + return this.clampTFTPrice(averagePrice, minPrice, maxPrice); + } + + /** + * tft_price = max(AverageTftPrice, MinTftPrice) then min(_, MaxTftPrice) + */ + private clampTFTPrice(avg: number, min: number, max: number): number { + let rate = avg; + if (rate < min) { + rate = min; + } + if (rate > max) { + rate = max; + } + return rate; + } } export { QueryTFTPrice }; From 583a5d28e1d661bdbcf3c164667d091d43a276cc Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Mon, 17 Nov 2025 13:29:27 +0200 Subject: [PATCH 2/3] Revert swap price changes --- packages/playground/src/components/TfSwapPrice.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playground/src/components/TfSwapPrice.vue b/packages/playground/src/components/TfSwapPrice.vue index 4a299addd5..ccbed90200 100644 --- a/packages/playground/src/components/TfSwapPrice.vue +++ b/packages/playground/src/components/TfSwapPrice.vue @@ -48,7 +48,7 @@ export default { setup() { const priceTask = useAsync( async () => { - const [price, error] = await resolveAsync(queryClient.tftPrice.getTFTBillingRate()); + const [price, error] = await resolveAsync(queryClient.tftPrice.get()); if (!price || error) { throw `Failed to load TFT price. Please click to reload.`; } From 090689175fe474f14717983a5db571c82900785f Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 19 Nov 2025 07:57:37 +0200 Subject: [PATCH 3/3] Rename billing rate fns based on returned unit, throw err instead of masking failure --- packages/grid_client/scripts/currency.ts | 2 +- .../src/clients/tf-grid/contracts.ts | 3 ++- .../src/clients/tf-grid/tftPrice.ts | 4 ++-- packages/grid_client/src/modules/calculator.ts | 8 ++++++-- .../src/weblets/tf_contracts_list.vue | 2 +- packages/tfchain_client/src/tft_price.ts | 18 ++++++++++++++---- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/grid_client/scripts/currency.ts b/packages/grid_client/scripts/currency.ts index e39c6bc306..c8b3dfd286 100644 --- a/packages/grid_client/scripts/currency.ts +++ b/packages/grid_client/scripts/currency.ts @@ -62,7 +62,7 @@ function yearlyUSD(hourlyUSD) { } async function main() { const grid = await getClient(); - const rate = await grid.tfclient.tftPrice.getTFTBillingRate(); + const rate = await grid.tfclient.tftPrice.getTFTBillingRateUSD(); const decimals = 3; currency = new TFTUSDConversionService(rate, decimals); diff --git a/packages/grid_client/src/clients/tf-grid/contracts.ts b/packages/grid_client/src/clients/tf-grid/contracts.ts index 03eaa1e628..32ddae1c3f 100644 --- a/packages/grid_client/src/clients/tf-grid/contracts.ts +++ b/packages/grid_client/src/clients/tf-grid/contracts.ts @@ -20,6 +20,7 @@ import { Decimal } from "decimal.js"; import { bytesToGB, formatErrorMessage } from "../../helpers"; import { calculator, ContractStates, currency } from "../../modules"; import { Graphql } from "../graphql/client"; +import { TFTPrice } from "./tftPrice"; export type DiscountLevel = "None" | "Default" | "Bronze" | "Silver" | "Gold"; @@ -387,7 +388,7 @@ class TFContracts extends Contracts { */ private async convertToTFT(USD: Decimal) { try { - const tftPrice = (await this.client.tftPrice.getTFTBillingRate()) ?? 0; + const tftPrice = await (this.client.tftPrice as TFTPrice).getTFTBillingRateUSD(); const tft = new currency(tftPrice, 15).convertUSDtoTFT({ amount: USD.toNumber() }); return new Decimal(tft); } catch (error) { diff --git a/packages/grid_client/src/clients/tf-grid/tftPrice.ts b/packages/grid_client/src/clients/tf-grid/tftPrice.ts index 9072cbe4d6..0f88feba5e 100644 --- a/packages/grid_client/src/clients/tf-grid/tftPrice.ts +++ b/packages/grid_client/src/clients/tf-grid/tftPrice.ts @@ -6,8 +6,8 @@ class TFTPrice extends QueryTFTPrice { return priceInMili / 1000; } - async getTFTBillingRate(): Promise { - const priceInMili = await super.getTFTBillingRate(); + async getTFTBillingRateUSD(): Promise { + const priceInMili = await super.getTFTBillingRateMUSD(); return priceInMili / 1000; } } diff --git a/packages/grid_client/src/modules/calculator.ts b/packages/grid_client/src/modules/calculator.ts index 986a23fc99..5f5e19af18 100644 --- a/packages/grid_client/src/modules/calculator.ts +++ b/packages/grid_client/src/modules/calculator.ts @@ -149,8 +149,12 @@ class Calculator { @expose @validateInput async tftPrice(): Promise { - const pricing = await this.client.tftPrice.getTFTBillingRate(); - return this.client instanceof TFClient ? pricing : pricing / 1000; + if (this.client instanceof TFClient) { + return await this.client.tftPrice.getTFTBillingRateUSD(); + } else { + const pricing = await this.client.tftPrice.getTFTBillingRateMUSD(); + return pricing / 1000; + } } /** diff --git a/packages/playground/src/weblets/tf_contracts_list.vue b/packages/playground/src/weblets/tf_contracts_list.vue index 1c61d9f348..21ab08597b 100644 --- a/packages/playground/src/weblets/tf_contracts_list.vue +++ b/packages/playground/src/weblets/tf_contracts_list.vue @@ -514,7 +514,7 @@ async function getTotalCost() { loadingTotalCost.value = true; const res = await gridProxyClient.twins.getConsumption(profileManager.profile!.twinId); totalTFT.value = +(res.last_hour_consumption || 0).toFixed(3); - const tftPrice = await queryClient.tftPrice.getTFTBillingRate(); + const tftPrice = await queryClient.tftPrice.getTFTBillingRateMUSD(); totalCostUSD.value = +(totalTFT.value * (tftPrice / 1000)).toFixed(3); } catch (error: any) { totalTFT.value = 0; diff --git a/packages/tfchain_client/src/tft_price.ts b/packages/tfchain_client/src/tft_price.ts index 72f56041d2..2d032fe9ec 100644 --- a/packages/tfchain_client/src/tft_price.ts +++ b/packages/tfchain_client/src/tft_price.ts @@ -13,16 +13,26 @@ class QueryTFTPrice { } @checkConnection - async getTFTBillingRate(): Promise { + async getTFTBillingRateMUSD(): Promise { const [minRes, maxRes, avgRes] = await Promise.all([ this.client.api.query.tftPriceModule.minTftPrice(), this.client.api.query.tftPriceModule.maxTftPrice(), this.client.api.query.tftPriceModule.averageTftPrice(), ]); - const minPrice = (minRes.toPrimitive() as number) ?? 0; - const maxPrice = (maxRes.toPrimitive() as number) ?? 0; - const averagePrice = (avgRes.toPrimitive() as number) ?? 0; + const minPrice = minRes.toPrimitive(); + const maxPrice = maxRes.toPrimitive(); + const averagePrice = avgRes.toPrimitive(); + + if (typeof minPrice !== "number" || isNaN(minPrice)) { + throw new Error("Failed to retrieve minimum TFT price: invalid value returned"); + } + if (typeof maxPrice !== "number" || isNaN(maxPrice)) { + throw new Error("Failed to retrieve maximum TFT price: invalid value returned"); + } + if (typeof averagePrice !== "number" || isNaN(averagePrice)) { + throw new Error("Failed to retrieve average TFT price: invalid value returned"); + } // Clamp the average between the configured bounds, matching on-chain billing logic return this.clampTFTPrice(averagePrice, minPrice, maxPrice);