From 3c5dc7c9cd27ad4c9c3d17d56fc026b38009a4f2 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 28 Feb 2023 11:15:18 +0100 Subject: [PATCH] feat: use TVL canister (#1953) # Motivation Replace web2 calls to display TVL in $ with [TVL canister](https://github.com/dfinity/ic/tree/master/rs/rosetta-api/tvl). # Changes - remove Binance and Governance `/metrics` rest API - implement TVL canister - add API and services to query TVL - integrate TVL in worker and post message - uses new TVL object in component - update periodicity to one hour to not refresh the value dynamically for now # Screenshot No visual UI changes. --- frontend/jest-setup.ts | 1 + frontend/src/lib/api/tvl.api.cjs.ts | 49 ++++++++ .../src/lib/canisters/tvl/tvl.canister.ts | 50 ++++++++ .../lib/canisters/tvl/tvl.canister.types.ts | 9 ++ .../lib/canisters/tvl/tvl.certified.idl.d.ts | 2 + .../lib/canisters/tvl/tvl.certified.idl.js | 33 ++++++ frontend/src/lib/canisters/tvl/tvl.did | 36 ++++++ frontend/src/lib/canisters/tvl/tvl.idl.d.ts | 2 + frontend/src/lib/canisters/tvl/tvl.idl.js | 33 ++++++ frontend/src/lib/canisters/tvl/tvl.types.ts | 32 +++++ .../metrics/TotalValueLocked.svelte | 10 +- .../lib/constants/canister-ids.constants.ts | 6 + .../lib/constants/environment.constants.ts | 2 + .../src/lib/constants/metrics.constants.ts | 6 +- frontend/src/lib/rest/binance.rest.ts | 31 ----- .../src/lib/rest/governance-metrics.rest.ts | 23 ---- .../$public/governance-metrics.services.ts | 56 --------- .../src/lib/services/$public/tvl.service.ts | 12 ++ frontend/src/lib/types/metrics.ts | 6 +- frontend/src/lib/workers/metrics.worker.ts | 27 +++-- frontend/src/tests/lib/api/tvl.api.spec.ts | 54 +++++++++ .../metrics/TotalValueLocked.spec.ts | 25 ++-- .../src/tests/lib/rest/binance.rest.spec.ts | 59 ---------- .../lib/rest/governance-metrics.rest.spec.ts | 60 ---------- .../governance-metrics.services.spec.ts | 109 ------------------ .../lib/services/_public/tvl.services.spec.ts | 30 +++++ 26 files changed, 378 insertions(+), 385 deletions(-) create mode 100644 frontend/src/lib/api/tvl.api.cjs.ts create mode 100644 frontend/src/lib/canisters/tvl/tvl.canister.ts create mode 100644 frontend/src/lib/canisters/tvl/tvl.canister.types.ts create mode 100644 frontend/src/lib/canisters/tvl/tvl.certified.idl.d.ts create mode 100644 frontend/src/lib/canisters/tvl/tvl.certified.idl.js create mode 100644 frontend/src/lib/canisters/tvl/tvl.did create mode 100644 frontend/src/lib/canisters/tvl/tvl.idl.d.ts create mode 100644 frontend/src/lib/canisters/tvl/tvl.idl.js create mode 100644 frontend/src/lib/canisters/tvl/tvl.types.ts delete mode 100644 frontend/src/lib/rest/binance.rest.ts delete mode 100644 frontend/src/lib/rest/governance-metrics.rest.ts delete mode 100644 frontend/src/lib/services/$public/governance-metrics.services.ts create mode 100644 frontend/src/lib/services/$public/tvl.service.ts create mode 100644 frontend/src/tests/lib/api/tvl.api.spec.ts delete mode 100644 frontend/src/tests/lib/rest/binance.rest.spec.ts delete mode 100644 frontend/src/tests/lib/rest/governance-metrics.rest.spec.ts delete mode 100644 frontend/src/tests/lib/services/_public/governance-metrics.services.spec.ts create mode 100644 frontend/src/tests/lib/services/_public/tvl.services.spec.ts diff --git a/frontend/jest-setup.ts b/frontend/jest-setup.ts index 94f08cdcf4b..fb6b4c16dc3 100644 --- a/frontend/jest-setup.ts +++ b/frontend/jest-setup.ts @@ -38,6 +38,7 @@ jest.mock("./src/lib/constants/canister-ids.constants.ts", () => ({ CKBTC_LEDGER_CANISTER_ID: Principal.fromText("mc6ru-gyaaa-aaaar-qaaaq-cai"), CKBTC_INDEX_CANISTER_ID: Principal.fromText("si2b5-pyaaa-aaaaa-aaaja-cai"), CKBTC_UNIVERSE_CANISTER_ID: Principal.fromText("mc6ru-gyaaa-aaaar-qaaaq-cai"), + TVL_CANISTER_ID: Principal.fromText("ewh3f-3qaaa-aaaap-aazjq-cai"), })); jest.mock("./src/lib/constants/environment.constants.ts", () => ({ diff --git a/frontend/src/lib/api/tvl.api.cjs.ts b/frontend/src/lib/api/tvl.api.cjs.ts new file mode 100644 index 00000000000..9a713b92c50 --- /dev/null +++ b/frontend/src/lib/api/tvl.api.cjs.ts @@ -0,0 +1,49 @@ +import { TVLCanister } from "$lib/canisters/tvl/tvl.canister"; +import type { TvlResult } from "$lib/canisters/tvl/tvl.types"; +import { TVL_CANISTER_ID } from "$lib/constants/canister-ids.constants"; +import { HOST_IC0_APP } from "$lib/constants/environment.constants"; +import { logWithTimestamp } from "$lib/utils/dev.utils"; +import type { Identity } from "@dfinity/agent"; +import type { Principal } from "@dfinity/principal"; +/** + * HTTP-Agent explicit CJS import for compatibility with web worker - avoid Error [RollupError]: Unexpected token (Note that you need plugins to import files that are not JavaScript) + */ +import { HttpAgent } from "@dfinity/agent/lib/cjs/index"; + +export const queryTVL = async ({ + identity, + certified, +}: { + identity: Identity; + certified: boolean; +}): Promise => { + logWithTimestamp(`Getting canister ${TVL_CANISTER_ID.toText()} TVL call...`); + + const { getTVL } = await canister({ identity, canisterId: TVL_CANISTER_ID }); + + const result = getTVL({ certified }); + + logWithTimestamp( + `Getting canister ${TVL_CANISTER_ID.toText()} TVL complete.` + ); + + return result; +}; + +const canister = async ({ + identity, + canisterId, +}: { + identity: Identity; + canisterId: Principal; +}): Promise => { + const agent = new HttpAgent({ + identity, + host: HOST_IC0_APP, + }); + + return TVLCanister.create({ + agent, + canisterId, + }); +}; diff --git a/frontend/src/lib/canisters/tvl/tvl.canister.ts b/frontend/src/lib/canisters/tvl/tvl.canister.ts new file mode 100644 index 00000000000..c9779f7991f --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.canister.ts @@ -0,0 +1,50 @@ +import type { TVLCanisterOptions } from "$lib/canisters/tvl/tvl.canister.types"; +import { Actor } from "@dfinity/agent"; +import { idlFactory as certifiedIdlFactory } from "./tvl.certified.idl"; +import { idlFactory } from "./tvl.idl"; +import type { TvlResult, _SERVICE as TVLService } from "./tvl.types"; + +export class TVLCanister { + private constructor( + private readonly service: TVLService, + private readonly certifiedService: TVLService + ) { + this.service = service; + this.certifiedService = certifiedService; + } + + public static create(options: TVLCanisterOptions) { + const agent = options.agent; + const canisterId = options.canisterId; + + const service = Actor.createActor(idlFactory, { + agent, + canisterId, + }); + + const certifiedService = Actor.createActor( + certifiedIdlFactory, + { + agent, + canisterId, + } + ); + + return new TVLCanister(service, certifiedService); + } + + private caller = ({ certified = true }: { certified: boolean }): TVLService => + certified ? this.certifiedService : this.service; + + public getTVL = async (params: { + certified: boolean; + }): Promise => { + const response = await this.caller(params).get_tvl(); + + if ("Err" in response) { + throw new Error(response.Err.message); + } + + return response.Ok; + }; +} diff --git a/frontend/src/lib/canisters/tvl/tvl.canister.types.ts b/frontend/src/lib/canisters/tvl/tvl.canister.types.ts new file mode 100644 index 00000000000..15949f26f4a --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.canister.types.ts @@ -0,0 +1,9 @@ +import type { Agent } from "@dfinity/agent"; +import type { Principal } from "@dfinity/principal"; + +export interface TVLCanisterOptions { + // The agent to use when communicating with the governance canister. + agent: Agent; + // The TVL canister's ID. + canisterId: Principal; +} diff --git a/frontend/src/lib/canisters/tvl/tvl.certified.idl.d.ts b/frontend/src/lib/canisters/tvl/tvl.certified.idl.d.ts new file mode 100644 index 00000000000..8e1474b8db6 --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.certified.idl.d.ts @@ -0,0 +1,2 @@ +import type { IDL } from "@dfinity/candid"; +export const idlFactory: IDL.InterfaceFactory; diff --git a/frontend/src/lib/canisters/tvl/tvl.certified.idl.js b/frontend/src/lib/canisters/tvl/tvl.certified.idl.js new file mode 100644 index 00000000000..4eb2e937f96 --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.certified.idl.js @@ -0,0 +1,33 @@ +/* Do not edit. Compiled with ./scripts/compile-idl-js from packages/tvl/candid/tvl.did */ +export const idlFactory = ({ IDL }) => { + const InitArgs = IDL.Record({ + governance_id: IDL.Principal, + update_period: IDL.Nat64, + xrc_id: IDL.Principal, + }); + const TimeseriesEntry = IDL.Record({ + value: IDL.Nat, + time_sec: IDL.Nat, + }); + const TimeseriesResult = IDL.Record({ + timeseries: IDL.Vec(TimeseriesEntry), + }); + const TvlResult = IDL.Record({ tvl: IDL.Nat, time_sec: IDL.Nat }); + const TvlResultError = IDL.Record({ message: IDL.Text }); + const Result_tvl = IDL.Variant({ Ok: TvlResult, Err: TvlResultError }); + const TvlTimeseriesResult = IDL.Record({ timeseries: IDL.Vec(TvlResult) }); + return IDL.Service({ + get_locked_e8s_timeseries: IDL.Func([], [TimeseriesResult], []), + get_tvl: IDL.Func([], [Result_tvl], []), + get_tvl_timeseries: IDL.Func([], [TvlTimeseriesResult], []), + get_xr_timeseries: IDL.Func([], [TimeseriesResult], []), + }); +}; +export const init = ({ IDL }) => { + const InitArgs = IDL.Record({ + governance_id: IDL.Principal, + update_period: IDL.Nat64, + xrc_id: IDL.Principal, + }); + return [InitArgs]; +}; diff --git a/frontend/src/lib/canisters/tvl/tvl.did b/frontend/src/lib/canisters/tvl/tvl.did new file mode 100644 index 00000000000..774ef7019bd --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.did @@ -0,0 +1,36 @@ +type InitArgs = record { + governance_id : principal; + xrc_id : principal; + update_period: nat64; +}; + +type TimeseriesEntry = record { + time_sec: nat; + value: nat; +}; + +type TimeseriesResult = record { + timeseries: vec TimeseriesEntry; +}; + +type TvlResult = record { + time_sec: nat; + tvl: nat; +}; + +type TvlTimeseriesResult = record { + timeseries: vec TvlResult; +}; + +type TvlResultError = record { + message: text; +}; + +type Result_tvl = variant { Ok : TvlResult; Err : TvlResultError }; + +service : (InitArgs) -> { + get_tvl : () -> (Result_tvl) query; + get_tvl_timeseries : () -> (TvlTimeseriesResult); + get_xr_timeseries : () -> (TimeseriesResult); + get_locked_e8s_timeseries : () -> (TimeseriesResult); +} \ No newline at end of file diff --git a/frontend/src/lib/canisters/tvl/tvl.idl.d.ts b/frontend/src/lib/canisters/tvl/tvl.idl.d.ts new file mode 100644 index 00000000000..8e1474b8db6 --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.idl.d.ts @@ -0,0 +1,2 @@ +import type { IDL } from "@dfinity/candid"; +export const idlFactory: IDL.InterfaceFactory; diff --git a/frontend/src/lib/canisters/tvl/tvl.idl.js b/frontend/src/lib/canisters/tvl/tvl.idl.js new file mode 100644 index 00000000000..9c289a4f4db --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.idl.js @@ -0,0 +1,33 @@ +/* Do not edit. Compiled with ./scripts/compile-idl-js from packages/tvl/candid/tvl.did */ +export const idlFactory = ({ IDL }) => { + const InitArgs = IDL.Record({ + governance_id: IDL.Principal, + update_period: IDL.Nat64, + xrc_id: IDL.Principal, + }); + const TimeseriesEntry = IDL.Record({ + value: IDL.Nat, + time_sec: IDL.Nat, + }); + const TimeseriesResult = IDL.Record({ + timeseries: IDL.Vec(TimeseriesEntry), + }); + const TvlResult = IDL.Record({ tvl: IDL.Nat, time_sec: IDL.Nat }); + const TvlResultError = IDL.Record({ message: IDL.Text }); + const Result_tvl = IDL.Variant({ Ok: TvlResult, Err: TvlResultError }); + const TvlTimeseriesResult = IDL.Record({ timeseries: IDL.Vec(TvlResult) }); + return IDL.Service({ + get_locked_e8s_timeseries: IDL.Func([], [TimeseriesResult], []), + get_tvl: IDL.Func([], [Result_tvl], ["query"]), + get_tvl_timeseries: IDL.Func([], [TvlTimeseriesResult], []), + get_xr_timeseries: IDL.Func([], [TimeseriesResult], []), + }); +}; +export const init = ({ IDL }) => { + const InitArgs = IDL.Record({ + governance_id: IDL.Principal, + update_period: IDL.Nat64, + xrc_id: IDL.Principal, + }); + return [InitArgs]; +}; diff --git a/frontend/src/lib/canisters/tvl/tvl.types.ts b/frontend/src/lib/canisters/tvl/tvl.types.ts new file mode 100644 index 00000000000..ee407ef72be --- /dev/null +++ b/frontend/src/lib/canisters/tvl/tvl.types.ts @@ -0,0 +1,32 @@ +import type { ActorMethod } from "@dfinity/agent"; +import type { Principal } from "@dfinity/principal"; + +export interface InitArgs { + governance_id: Principal; + update_period: bigint; + xrc_id: Principal; +} +export type Result_tvl = { Ok: TvlResult } | { Err: TvlResultError }; +export interface TimeseriesEntry { + value: bigint; + time_sec: bigint; +} +export interface TimeseriesResult { + timeseries: Array; +} +export interface TvlResult { + tvl: bigint; + time_sec: bigint; +} +export interface TvlResultError { + message: string; +} +export interface TvlTimeseriesResult { + timeseries: Array; +} +export interface _SERVICE { + get_locked_e8s_timeseries: ActorMethod<[], TimeseriesResult>; + get_tvl: ActorMethod<[], Result_tvl>; + get_tvl_timeseries: ActorMethod<[], TvlTimeseriesResult>; + get_xr_timeseries: ActorMethod<[], TimeseriesResult>; +} diff --git a/frontend/src/lib/components/metrics/TotalValueLocked.svelte b/frontend/src/lib/components/metrics/TotalValueLocked.svelte index ead0f94b2d3..2903a5acd80 100644 --- a/frontend/src/lib/components/metrics/TotalValueLocked.svelte +++ b/frontend/src/lib/components/metrics/TotalValueLocked.svelte @@ -10,7 +10,6 @@ import { fade } from "svelte/transition"; import { nonNullish } from "@dfinity/utils"; import { metricsStore } from "$lib/stores/metrics.store"; - import { E8S_PER_ICP } from "$lib/constants/icp.constants"; import { formatNumber } from "$lib/utils/format.utils"; export let layout: "inline" | "stacked" = "inline"; @@ -34,15 +33,8 @@ const syncMetrics = ({ metrics: data }: PostMessageDataResponse) => metricsStore.set(data); - let totalNeurons: number | undefined; - $: totalNeurons = - ($metricsStore?.dissolvingNeurons?.totalDissolvingNeurons ?? 0) + - ($metricsStore?.dissolvingNeurons?.totalNotDissolvingNeurons ?? 0); - let total: number | undefined; - $: total = - ((totalNeurons ?? 0) / E8S_PER_ICP) * - Number($metricsStore?.avgPrice?.price ?? "0"); + $: total = Number($metricsStore?.tvl?.tvl ?? "0"); const format = (n: number): string => formatNumber(n, { diff --git a/frontend/src/lib/constants/canister-ids.constants.ts b/frontend/src/lib/constants/canister-ids.constants.ts index 157c98a146a..ced1b80b8a3 100644 --- a/frontend/src/lib/constants/canister-ids.constants.ts +++ b/frontend/src/lib/constants/canister-ids.constants.ts @@ -41,3 +41,9 @@ export const CKBTC_INDEX_CANISTER_ID = Principal.fromText( : MAINNET_CKBTC_INDEX_CANISTER_ID ); export const CKBTC_UNIVERSE_CANISTER_ID = CKBTC_LEDGER_CANISTER_ID; + +// TVL Canister ID on mainnet. Use for readonly. + +export const TVL_CANISTER_ID = Principal.fromText( + "ewh3f-3qaaa-aaaap-aazjq-cai" +); diff --git a/frontend/src/lib/constants/environment.constants.ts b/frontend/src/lib/constants/environment.constants.ts index 8eedd53e4eb..d18a5c6cf1f 100644 --- a/frontend/src/lib/constants/environment.constants.ts +++ b/frontend/src/lib/constants/environment.constants.ts @@ -4,6 +4,8 @@ export const DEV = import.meta.env.DEV; export const FETCH_ROOT_KEY: boolean = import.meta.env.VITE_FETCH_ROOT_KEY === "true"; +export const HOST_IC0_APP = "https://ic0.app"; + // TODO: Add as env var https://dfinity.atlassian.net/browse/GIX-1245 // Local development needs `.raw` to avoid CORS issues for now. // TODO: Fix CORS issues diff --git a/frontend/src/lib/constants/metrics.constants.ts b/frontend/src/lib/constants/metrics.constants.ts index 894268e0592..c4bc27b41ab 100644 --- a/frontend/src/lib/constants/metrics.constants.ts +++ b/frontend/src/lib/constants/metrics.constants.ts @@ -1,4 +1,6 @@ import { SECONDS_IN_MINUTE } from "$lib/constants/constants"; -// Workers -export const SYNC_METRICS_TIMER_INTERVAL = SECONDS_IN_MINUTE * 1000; // 1 minute +// Workers periodicity +// 60 minutes - i.e. currently longer than a session therefore not refreshed +// We might revert this to a more dynamic data in the future e.g. every minute, that's why we keep the feature +export const SYNC_METRICS_TIMER_INTERVAL = SECONDS_IN_MINUTE * 60000; diff --git a/frontend/src/lib/rest/binance.rest.ts b/frontend/src/lib/rest/binance.rest.ts deleted file mode 100644 index e1fef4147d8..00000000000 --- a/frontend/src/lib/rest/binance.rest.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { BinanceAvgPrice } from "$lib/types/binance"; - -/** - * Current average price for a symbol - ICP - provided by Binance. - * - * Documentation: - * - https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#general-api-information - * - https://binance-docs.github.io/apidocs/spot/en/#current-average-price - * - */ -export const exchangeRateICPToUsd = - async (): Promise => { - try { - // TODO: extract .env - const response = await fetch( - "https://api.binance.com/api/v3/avgPrice?symbol=ICPUSDT" - ); - - if (!response.ok) { - // We silence any error here - if no result is found, no informative information shall be displayed - console.error("Error fetching symbol average price", response); - return null; - } - - return response.json(); - } catch (err: unknown) { - // We silence any error here - if no result is found, no informative information shall be displayed - console.error("Unexpected error fetching symbol average price", err); - return null; - } - }; diff --git a/frontend/src/lib/rest/governance-metrics.rest.ts b/frontend/src/lib/rest/governance-metrics.rest.ts deleted file mode 100644 index 7862d82b243..00000000000 --- a/frontend/src/lib/rest/governance-metrics.rest.ts +++ /dev/null @@ -1,23 +0,0 @@ -// TODO: Extract .env -const GOVERNANCE_CANISTER_RAW_URL = - "https://rrkah-fqaaa-aaaaa-aaaaq-cai.raw.ic0.app"; - -/** - * Metrics of the Governance canister. - */ -export const governanceMetrics = async (): Promise => { - try { - const response = await fetch(`${GOVERNANCE_CANISTER_RAW_URL}/metrics`); - if (!response.ok) { - // We silence any error here - if no result is found, no informative information shall be displayed - console.error("Error fetching the governance metrics", response); - return null; - } - - return response.text(); - } catch (err: unknown) { - // We silence any error here - if no result is found, no informative information shall be displayed - console.error("Unexpected error fetching the governance metrics", err); - return null; - } -}; diff --git a/frontend/src/lib/services/$public/governance-metrics.services.ts b/frontend/src/lib/services/$public/governance-metrics.services.ts deleted file mode 100644 index 8e9a7119b65..00000000000 --- a/frontend/src/lib/services/$public/governance-metrics.services.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { governanceMetrics } from "$lib/rest/governance-metrics.rest"; -import type { DissolvingNeurons } from "$lib/types/governance-metrics"; - -const GOVERNANCE_DISSOLVING_NEURONS_E8S_COUNT_KEY = - "governance_dissolving_neurons_e8s_count"; -const GOVERNANCE_NOT_DISSOLVING_NEURONS_E8S_COUNT_KEY = - "governance_not_dissolving_neurons_e8s_count"; - -/** - * Find the total dissolving neurons and not dissolving neurons in governance raw metrics data. - * - * Relevant are [key value timestamp]: - * governance_dissolving_neurons_e8s_count 8128821049483880 1674059025086 - * governance_not_dissolving_neurons_e8s_count 18188426841529904 1674059025086 - * - * P.S.: We only search for the value and do not consider the timestamp as we refresh the data every minutes anyway. - */ -export const totalDissolvingNeurons = - async (): Promise => { - const metrics = await governanceMetrics(); - - if (metrics === null) { - return null; - } - - const splits = metrics.replaceAll(/[\n\r]/g, " ").split(" "); - - const findValue = (key: string): string | undefined => { - const index = splits.findIndex((text: string) => text === key); - - if (index === -1) { - return undefined; - } - - return splits[index + 1]; - }; - - const totalDissolvingNeurons = findValue( - GOVERNANCE_DISSOLVING_NEURONS_E8S_COUNT_KEY - ); - const totalNotDissolvingNeurons = findValue( - GOVERNANCE_NOT_DISSOLVING_NEURONS_E8S_COUNT_KEY - ); - - const valid = (metric: string | undefined): boolean => - metric !== undefined && !isNaN(Number(metric)); - - if (valid(totalDissolvingNeurons) && valid(totalNotDissolvingNeurons)) { - return { - totalDissolvingNeurons: Number(totalDissolvingNeurons), - totalNotDissolvingNeurons: Number(totalNotDissolvingNeurons), - }; - } - - return null; - }; diff --git a/frontend/src/lib/services/$public/tvl.service.ts b/frontend/src/lib/services/$public/tvl.service.ts new file mode 100644 index 00000000000..7638655e817 --- /dev/null +++ b/frontend/src/lib/services/$public/tvl.service.ts @@ -0,0 +1,12 @@ +import { queryTVL as queryTVLApi } from "$lib/api/tvl.api.cjs"; +import type { TvlResult } from "$lib/canisters/tvl/tvl.types"; +import { AnonymousIdentity } from "@dfinity/agent"; + +export const queryTVL = (): Promise => + queryTVLApi({ + // Because we use the service in a web worker. + // Do not use utils to generate anonymous identity to avoid Vite/Rollup build issue: + // "Unexpected token (Note that you need plugins to import files that are not JavaScript)" + identity: new AnonymousIdentity(), + certified: false, + }); diff --git a/frontend/src/lib/types/metrics.ts b/frontend/src/lib/types/metrics.ts index efbbabffecf..3a3d86c2384 100644 --- a/frontend/src/lib/types/metrics.ts +++ b/frontend/src/lib/types/metrics.ts @@ -1,7 +1,5 @@ -import type { BinanceAvgPrice } from "$lib/types/binance"; -import type { DissolvingNeurons } from "$lib/types/governance-metrics"; +import type { TvlResult } from "$lib/canisters/tvl/tvl.types"; export interface MetricsSync { - avgPrice: BinanceAvgPrice | null; - dissolvingNeurons: DissolvingNeurons | null; + tvl?: TvlResult; } diff --git a/frontend/src/lib/workers/metrics.worker.ts b/frontend/src/lib/workers/metrics.worker.ts index 399d6ce4fc6..5f90d9d519b 100644 --- a/frontend/src/lib/workers/metrics.worker.ts +++ b/frontend/src/lib/workers/metrics.worker.ts @@ -1,8 +1,6 @@ +import type { TvlResult } from "$lib/canisters/tvl/tvl.types"; import { SYNC_METRICS_TIMER_INTERVAL } from "$lib/constants/metrics.constants"; -import { exchangeRateICPToUsd } from "$lib/rest/binance.rest"; -import { totalDissolvingNeurons } from "$lib/services/$public/governance-metrics.services"; -import type { BinanceAvgPrice } from "$lib/types/binance"; -import type { DissolvingNeurons } from "$lib/types/governance-metrics"; +import { queryTVL } from "$lib/services/$public/tvl.service"; import type { PostMessage, PostMessageDataRequest, @@ -58,23 +56,24 @@ const syncMetrics = async () => { syncInProgress = true; - const [avgPrice, dissolvingNeurons] = await Promise.all([ - exchangeRateICPToUsd(), - totalDissolvingNeurons(), - ]); + try { + const tvl = await queryTVL(); - emitCanister({ avgPrice, dissolvingNeurons }); + emitCanister(tvl); + } catch (err: unknown) { + // We silence the error here as it is not an information crucial for the usage of the dapp + console.error(err); + } syncInProgress = false; }; -const emitCanister = (metrics: { - avgPrice: BinanceAvgPrice | null; - dissolvingNeurons: DissolvingNeurons | null; -}) => +const emitCanister = (tvl: TvlResult) => postMessage({ msg: "nnsSyncMetrics", data: { - metrics, + metrics: { + tvl, + }, }, }); diff --git a/frontend/src/tests/lib/api/tvl.api.spec.ts b/frontend/src/tests/lib/api/tvl.api.spec.ts new file mode 100644 index 00000000000..512ec2c85c9 --- /dev/null +++ b/frontend/src/tests/lib/api/tvl.api.spec.ts @@ -0,0 +1,54 @@ +import { queryTVL } from "$lib/api/tvl.api.cjs"; +import { TVLCanister } from "$lib/canisters/tvl/tvl.canister"; +import { AnonymousIdentity } from "@dfinity/agent"; +import mock from "jest-mock-extended/lib/Mock"; + +jest.mock("@dfinity/agent", () => { + const agent = jest.requireActual("@dfinity/agent"); + return { + ...agent, + HttpAgent: jest.fn().mockImplementation(() => { + return {}; + }), + }; +}); + +describe("tvl api", () => { + const tvlCanisterMock = mock(); + + beforeAll(() => { + jest.spyOn(TVLCanister, "create").mockImplementation(() => tvlCanisterMock); + }); + + afterAll(() => jest.clearAllMocks()); + + const params = { + identity: new AnonymousIdentity(), + certified: true, + }; + + it("returns the tvl", async () => { + const result = { + tvl: 1n, + time_sec: 0n, + }; + + const getTVLSpy = tvlCanisterMock.getTVL.mockResolvedValue(result); + + const response = await queryTVL(params); + + expect(response).toEqual(result); + + expect(getTVLSpy).toBeCalled(); + }); + + it("throws an error if no token", () => { + tvlCanisterMock.getTVL.mockImplementation(async () => { + throw new Error(); + }); + + const call = () => queryTVL(params); + + expect(call).rejects.toThrowError(); + }); +}); diff --git a/frontend/src/tests/lib/components/metrics/TotalValueLocked.spec.ts b/frontend/src/tests/lib/components/metrics/TotalValueLocked.spec.ts index 51bd2a68bc7..8ee9d826bb6 100644 --- a/frontend/src/tests/lib/components/metrics/TotalValueLocked.spec.ts +++ b/frontend/src/tests/lib/components/metrics/TotalValueLocked.spec.ts @@ -5,8 +5,6 @@ import TotalValueLocked from "$lib/components/metrics/TotalValueLocked.svelte"; import type { MetricsCallback } from "$lib/services/$public/worker-metrics.services"; import { metricsStore } from "$lib/stores/metrics.store"; -import type { BinanceAvgPrice } from "$lib/types/binance"; -import type { DissolvingNeurons } from "$lib/types/governance-metrics"; import { render, waitFor } from "@testing-library/svelte"; import { tick } from "svelte"; @@ -33,22 +31,20 @@ describe("TotalValueLocked", () => { jest.resetAllMocks(); }); + const tvl = { + tvl: 442469700n, + time_sec: 123n, + }; + it("should render TVL", async () => { const { getByTestId } = render(TotalValueLocked); // Wait for initialization of the callback await waitFor(() => expect(metricsCallback).not.toBeUndefined()); - const avgPrice: BinanceAvgPrice = { mins: 5, price: "5.42963025" }; - const dissolvingNeurons: DissolvingNeurons = { - totalDissolvingNeurons: 8147494574194015, - totalNotDissolvingNeurons: 1674035200397, - }; - metricsCallback?.({ metrics: { - avgPrice, - dissolvingNeurons, + tvl, }, }); @@ -77,16 +73,9 @@ describe("TotalValueLocked", () => { await tick(); - const avgPrice: BinanceAvgPrice = { mins: 5, price: "0" }; - const dissolvingNeurons: DissolvingNeurons = { - totalDissolvingNeurons: 0, - totalNotDissolvingNeurons: 0, - }; - metricsCallback?.({ metrics: { - avgPrice, - dissolvingNeurons, + tvl, }, }); diff --git a/frontend/src/tests/lib/rest/binance.rest.spec.ts b/frontend/src/tests/lib/rest/binance.rest.spec.ts deleted file mode 100644 index 10e92c32860..00000000000 --- a/frontend/src/tests/lib/rest/binance.rest.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { exchangeRateICPToUsd } from "$lib/rest/binance.rest"; -import type { BinanceAvgPrice } from "$lib/types/binance"; - -describe("Binance Rest API", () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.spyOn(console, "error").mockImplementation(() => undefined); - }); - - it("should return an average price", async () => { - const data: BinanceAvgPrice = { mins: 5, price: "5.43853359" }; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore mock fetch - global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve(data), - ok: true, - status: 200, - }) - ); - - const rate = await exchangeRateICPToUsd(); - - expect(rate.mins).toEqual(data.mins); - expect(rate.price).toEqual(data.price); - - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it("should return null if return code invalid", async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore mock fetch - global.fetch = jest.fn(() => - Promise.resolve({ - ok: false, - status: 403, - }) - ); - - const rate = await exchangeRateICPToUsd(); - - expect(rate).toBeNull(); - - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it("should return null if endpoint throws an exception", async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore mock fetch - global.fetch = jest.fn(() => Promise.reject("An API error")); - - const rate = await exchangeRateICPToUsd(); - - expect(rate).toBeNull(); - - expect(fetch).toHaveBeenCalledTimes(1); - }); -}); diff --git a/frontend/src/tests/lib/rest/governance-metrics.rest.spec.ts b/frontend/src/tests/lib/rest/governance-metrics.rest.spec.ts deleted file mode 100644 index 2c39c096bc2..00000000000 --- a/frontend/src/tests/lib/rest/governance-metrics.rest.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { governanceMetrics } from "$lib/rest/governance-metrics.rest"; -import { governanceMetricsText } from "../../mocks/metrics.mock"; - -describe("Governance metrics", () => { - beforeEach(() => - jest.spyOn(console, "error").mockImplementation(() => undefined) - ); - - afterAll(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - }); - - it("should return metrics as text", async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore mock fetch - global.fetch = jest.fn(() => - Promise.resolve({ - text: () => Promise.resolve(governanceMetricsText), - ok: true, - status: 200, - }) - ); - - const metrics = await governanceMetrics(); - - expect(metrics).toEqual(governanceMetricsText); - - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it("should return null if return code is invalid", async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore mock fetch - global.fetch = jest.fn(() => - Promise.resolve({ - ok: false, - status: 403, - }) - ); - - const metrics = await governanceMetrics(); - - expect(metrics).toBeNull(); - - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it("should return null if endpoint throws an exception", async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore mock fetch - global.fetch = jest.fn(() => Promise.reject("An API error")); - - const metrics = await governanceMetrics(); - - expect(metrics).toBeNull(); - - expect(fetch).toHaveBeenCalledTimes(1); - }); -}); diff --git a/frontend/src/tests/lib/services/_public/governance-metrics.services.spec.ts b/frontend/src/tests/lib/services/_public/governance-metrics.services.spec.ts deleted file mode 100644 index 2dbd2062ef6..00000000000 --- a/frontend/src/tests/lib/services/_public/governance-metrics.services.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as rest from "$lib/rest/governance-metrics.rest"; -import { totalDissolvingNeurons } from "$lib/services/$public/governance-metrics.services"; -import { governanceMetricsText } from "../../../mocks/metrics.mock"; - -describe("governance-metrics", () => { - afterEach(() => jest.clearAllMocks()); - - it("should return null if no API results", async () => { - const metricsRestSpy = jest - .spyOn(rest, "governanceMetrics") - .mockImplementation(() => Promise.resolve(null)); - - const result = await totalDissolvingNeurons(); - - expect(result).toBeNull(); - expect(metricsRestSpy).toHaveBeenCalled(); - }); - - it("should return null if no corresponding text key is found", async () => { - const metricsRestSpy = jest - .spyOn(rest, "governanceMetrics") - .mockImplementation(() => Promise.resolve(governanceMetricsText)); - - const result = await totalDissolvingNeurons(); - - expect(result).toBeNull(); - expect(metricsRestSpy).toHaveBeenCalled(); - }); - - it("should return null if no valid numbers are found", async () => { - const text = `${governanceMetricsText} -governance_dissolving_neurons_e8s_count test test`; - - const metricsRestSpy = jest - .spyOn(rest, "governanceMetrics") - .mockImplementation(() => Promise.resolve(text)); - - const result = await totalDissolvingNeurons(); - - expect(result).toBeNull(); - expect(metricsRestSpy).toHaveBeenCalled(); - }); - - it("should return null if no numbers are found", async () => { - const text = `${governanceMetricsText} -governance_dissolving_neurons_e8s_count -# HELP governance_dissolving_neurons_count Total number of dissolving neurons, grouped by dissolve delay (in years)`; - - const metricsRestSpy = jest - .spyOn(rest, "governanceMetrics") - .mockImplementation(() => Promise.resolve(text)); - - const result = await totalDissolvingNeurons(); - - expect(result).toBeNull(); - expect(metricsRestSpy).toHaveBeenCalled(); - }); - - it("should return null if only dissolving number is found", async () => { - const text = `${governanceMetricsText} -governance_dissolving_neurons_e8s_count 8147494574194015 1674059025086 -# HELP governance_dissolving_neurons_count Total number of dissolving neurons, grouped by dissolve delay (in years)`; - - const metricsRestSpy = jest - .spyOn(rest, "governanceMetrics") - .mockImplementation(() => Promise.resolve(text)); - - const result = await totalDissolvingNeurons(); - - expect(result).toBeNull(); - expect(metricsRestSpy).toHaveBeenCalled(); - }); - - it("should return null if only not dissolving number is found", async () => { - const text = `${governanceMetricsText} -governance_not_dissolving_neurons_e8s_count 18188426841529904 1674059025086 -# HELP governance_not_dissolving_neurons_count Total number of not dissolving neurons, grouped by dissolve delay (in years)`; - - const metricsRestSpy = jest - .spyOn(rest, "governanceMetrics") - .mockImplementation(() => Promise.resolve(text)); - - const result = await totalDissolvingNeurons(); - - expect(result).toBeNull(); - expect(metricsRestSpy).toHaveBeenCalled(); - }); - - it("should return dissolving metrics", async () => { - const totalDissolving = 8147494574194015; - const totalNotDissolving = 1674031880551; - - const text = `${governanceMetricsText} -governance_dissolving_neurons_e8s_count ${totalDissolving} 1674059025086 -# HELP governance_dissolving_neurons_count Total number of dissolving neurons, grouped by dissolve delay (in years) -governance_not_dissolving_neurons_e8s_count ${totalNotDissolving} 1674059025086 -# HELP governance_not_dissolving_neurons_count Total number of not dissolving neurons, grouped by dissolve delay (in years)`; - - const metricsRestSpy = jest - .spyOn(rest, "governanceMetrics") - .mockImplementation(() => Promise.resolve(text)); - - const result = await totalDissolvingNeurons(); - - expect(result.totalDissolvingNeurons).toEqual(totalDissolving); - expect(result.totalNotDissolvingNeurons).toEqual(totalNotDissolving); - expect(metricsRestSpy).toHaveBeenCalled(); - }); -}); diff --git a/frontend/src/tests/lib/services/_public/tvl.services.spec.ts b/frontend/src/tests/lib/services/_public/tvl.services.spec.ts new file mode 100644 index 00000000000..e8e0fd08104 --- /dev/null +++ b/frontend/src/tests/lib/services/_public/tvl.services.spec.ts @@ -0,0 +1,30 @@ +/** + * @jest-environment jsdom + */ + +import * as tvlApi from "$lib/api/tvl.api.cjs"; +import { queryTVL } from "$lib/services/$public/tvl.service"; +import { AnonymousIdentity } from "@dfinity/agent"; +import { waitFor } from "@testing-library/svelte"; + +describe("tvl services", () => { + const result = { + tvl: 1n, + time_sec: 0n, + }; + + it("should get tvl", async () => { + const spyQueryTVL = jest + .spyOn(tvlApi, "queryTVL") + .mockResolvedValue(result); + + await queryTVL(); + + await waitFor(() => + expect(spyQueryTVL).toBeCalledWith({ + identity: new AnonymousIdentity(), + certified: false, + }) + ); + }); +});