diff --git a/CHANGELOG-Nns-Dapp-unreleased.md b/CHANGELOG-Nns-Dapp-unreleased.md index 2f5f7a2595e..92b5216c01a 100644 --- a/CHANGELOG-Nns-Dapp-unreleased.md +++ b/CHANGELOG-Nns-Dapp-unreleased.md @@ -38,6 +38,10 @@ proposal is successful, the changes it released will be moved from this file to #### Security +- Read the SNS swap participant count only from the certified swap canister + state. Before, a swap without that field read the count from the unverified + raw metrics page. + #### Not Published ### Operations diff --git a/frontend/src/lib/api/sns-swap-metrics.api.ts b/frontend/src/lib/api/sns-swap-metrics.api.ts deleted file mode 100644 index 7277c113cae..00000000000 --- a/frontend/src/lib/api/sns-swap-metrics.api.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { logWithTimestamp } from "$lib/utils/dev.utils"; -import type { Principal } from "@icp-sdk/core/principal"; - -export const querySnsSwapMetrics = async ({ - swapCanisterId, -}: { - swapCanisterId: Principal; -}): Promise => { - logWithTimestamp("Loading SNS metrics..."); - - try { - // TODO: switch to a metrics canister. Otherwise not testable on testnet. - const url = `https://${swapCanisterId.toText()}.raw.icp0.io/metrics`; - const response = await fetch(url); - - if (!response.ok) { - throw new Error("response not ok"); - } - - const allMetrics = await response.text(); - - logWithTimestamp("Loading SNS metrics completed"); - - return allMetrics; - } catch (err) { - logWithTimestamp("Error getting SNS metrics", err); - } -}; diff --git a/frontend/src/lib/components/project-detail/ProjectCommitment.svelte b/frontend/src/lib/components/project-detail/ProjectCommitment.svelte index 48cf3cddb5c..5e7896f7a12 100644 --- a/frontend/src/lib/components/project-detail/ProjectCommitment.svelte +++ b/frontend/src/lib/components/project-detail/ProjectCommitment.svelte @@ -5,7 +5,6 @@ import NfCommitmentProgressBar from "$lib/components/project-detail/NfCommitmentProgressBar.svelte"; import { getMaxNeuronsFundParticipation } from "$lib/getters/sns-summary"; import { i18n } from "$lib/stores/i18n"; - import { snsSwapMetricsStore } from "$lib/stores/sns-swap-metrics.store"; import { PROJECT_DETAIL_CONTEXT_KEY, type ProjectDetailContext, @@ -54,8 +53,6 @@ let saleBuyerCount: number | undefined; $: saleBuyerCount = swapSaleBuyerCount({ - rootCanisterId: $projectDetailStore?.summary?.rootCanisterId, - swapMetrics: $snsSwapMetricsStore, derivedState: summary.derived, }); diff --git a/frontend/src/lib/pages/ProjectDetail.svelte b/frontend/src/lib/pages/ProjectDetail.svelte index dea1253a4ac..463f6ccf9eb 100644 --- a/frontend/src/lib/pages/ProjectDetail.svelte +++ b/frontend/src/lib/pages/ProjectDetail.svelte @@ -16,7 +16,6 @@ hidePollingToast, restoreSnsSaleParticipation, } from "$lib/services/sns-sale.services"; - import { loadSnsSwapMetrics } from "$lib/services/sns-swap-metrics.services"; import { loadSnsDerivedState, loadSnsLifecycle, @@ -39,7 +38,6 @@ } from "$lib/types/project-detail.context"; import { SaleStep } from "$lib/types/sale"; import { userCountryIsNeeded } from "$lib/utils/projects.utils"; - import { hasBuyersCount } from "$lib/utils/sns-swap.utils"; import { getCommitmentE8s } from "$lib/utils/sns.utils"; import { Principal } from "@icp-sdk/core/principal"; import { SnsSwapLifecycle } from "@icp-sdk/canisters/sns"; @@ -183,28 +181,14 @@ }); } - let derivedStateHasBuyersCount: boolean | undefined; - $: derivedStateHasBuyersCount = hasBuyersCount( - $projectDetailStore?.summary?.derived - ); let areWatchersSet = false; let unsubscribeWatchCommitment: () => void | undefined; $: if ( nonNullish(rootCanisterId) && nonNullish(swapCanisterId) && - nonNullish(derivedStateHasBuyersCount) && !areWatchersSet ) { - if (!derivedStateHasBuyersCount) { - // TODO: Remove once Dragginz, OC and SONIC support new fields in in SnsGetDerivedStateResponse - loadSnsSwapMetrics({ - rootCanisterId: Principal.fromText(rootCanisterId), - swapCanisterId, - forceFetch: false, - }); - } - if (enableOpenProjectWatchers) { areWatchersSet = true; unsubscribeWatchCommitment?.(); diff --git a/frontend/src/lib/services/sns-swap-metrics.services.ts b/frontend/src/lib/services/sns-swap-metrics.services.ts deleted file mode 100644 index 4b023afff24..00000000000 --- a/frontend/src/lib/services/sns-swap-metrics.services.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { querySnsSwapMetrics } from "$lib/api/sns-swap-metrics.api"; -import { snsSwapMetricsStore } from "$lib/stores/sns-swap-metrics.store"; -import { parseSnsSwapSaleBuyerCount } from "$lib/utils/sns.utils"; -import type { Principal } from "@icp-sdk/core/principal"; -import { get } from "svelte/store"; - -/** - * Get metrics from the store or fetch it - * @param rootCanisterId - */ -export const loadSnsSwapMetrics = async ({ - rootCanisterId, - swapCanisterId, - forceFetch, -}: { - rootCanisterId: Principal; - swapCanisterId: Principal; - forceFetch: boolean; -}): Promise => { - const store = get(snsSwapMetricsStore); - - // skip update when data is available - if (!forceFetch && store[rootCanisterId.toText()] !== undefined) { - return; - } - - if (store[rootCanisterId.toText()] === undefined) { - // mark in progress to avoid multiple load - snsSwapMetricsStore.setMetrics({ - rootCanisterId, - metrics: null, - }); - } - - const rawMetrics = await querySnsSwapMetrics({ swapCanisterId }); - if (rawMetrics === undefined) { - return; - } - - const saleBuyerCount = parseSnsSwapSaleBuyerCount(rawMetrics); - if (saleBuyerCount === undefined) { - return; - } - - snsSwapMetricsStore.setMetrics({ - rootCanisterId, - metrics: { saleBuyerCount }, - }); -}; diff --git a/frontend/src/lib/stores/sns-swap-metrics.store.ts b/frontend/src/lib/stores/sns-swap-metrics.store.ts deleted file mode 100644 index 04bfcd9f6ef..00000000000 --- a/frontend/src/lib/stores/sns-swap-metrics.store.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { RootCanisterIdText } from "$lib/types/sns"; -import type { Principal } from "@icp-sdk/core/principal"; -import type { Readable } from "svelte/store"; -import { writable } from "svelte/store"; - -/** - * undefined - not initialized, null - not found - */ -export interface SnsSwapMetrics { - saleBuyerCount: number; -} - -export type SnsSwapMetricsStoreData = Record< - RootCanisterIdText, - SnsSwapMetrics | undefined | null ->; - -export interface SnsSwapMetricsStore extends Readable { - setMetrics: (data: { - rootCanisterId: Principal; - metrics: SnsSwapMetrics | undefined | null; - }) => void; - reset: () => void; -} - -const initSnsSwapMetricsStore = (): SnsSwapMetricsStore => { - const { subscribe, update, set } = writable({}); - - return { - subscribe, - - setMetrics({ - rootCanisterId, - metrics, - }: { - rootCanisterId: Principal; - metrics: SnsSwapMetrics | null | undefined; - }) { - update((currentState: SnsSwapMetricsStoreData) => ({ - ...currentState, - [rootCanisterId.toText()]: metrics, - })); - }, - - // Used in tests - reset() { - set({}); - }, - }; -}; - -export const snsSwapMetricsStore = initSnsSwapMetricsStore(); diff --git a/frontend/src/lib/utils/sns-swap.utils.ts b/frontend/src/lib/utils/sns-swap.utils.ts index 7be1c7a83ff..39faed5facc 100644 --- a/frontend/src/lib/utils/sns-swap.utils.ts +++ b/frontend/src/lib/utils/sns-swap.utils.ts @@ -1,38 +1,17 @@ -import type { SnsSwapMetricsStoreData } from "$lib/stores/sns-swap-metrics.store"; import { fromNullable, nonNullish } from "@dfinity/utils"; import type { SnsSwapDid } from "@icp-sdk/canisters/sns"; -import type { Principal } from "@icp-sdk/core/principal"; +/** + * Returns the number of direct participants of a swap. + * + * The value comes from the certified `get_derived_state` call. It is undefined + * when the swap canister does not report the field. + */ export const swapSaleBuyerCount = ({ - swapMetrics, - rootCanisterId, derivedState: { direct_participant_count }, }: { - swapMetrics: SnsSwapMetricsStoreData; - rootCanisterId: Principal | undefined; derivedState: SnsSwapDid.DerivedState; }): number | undefined => { - if (nonNullish(fromNullable(direct_participant_count))) { - return Number(fromNullable(direct_participant_count)); - } - return rootCanisterId === undefined - ? undefined - : swapMetrics?.[rootCanisterId.toText()]?.saleBuyerCount; -}; - -/** - * Returns whether the derived state has a buyers count. - * - * It returns undefined if the derived state is undefined or null. - * - * If the field is not set, we want to trigger a call to the raw canister metrics. - * Therefore, we don't want to return `false` while the derived state is not present. - */ -export const hasBuyersCount = ( - derived: SnsSwapDid.DerivedState | undefined | null -): undefined | boolean => { - if (derived === undefined || derived === null) { - return undefined; - } - return nonNullish(fromNullable(derived.direct_participant_count)); + const count = fromNullable(direct_participant_count); + return nonNullish(count) ? Number(count) : undefined; }; diff --git a/frontend/src/lib/utils/sns.utils.ts b/frontend/src/lib/utils/sns.utils.ts index 06cf85f3626..fdc061e594f 100644 --- a/frontend/src/lib/utils/sns.utils.ts +++ b/frontend/src/lib/utils/sns.utils.ts @@ -100,28 +100,6 @@ export const hasOpenTicketInProcess = ({ return { status: "loading" }; }; -/** - * Parse the `sale_buyer_count` value from metrics text. - * - * @example text - * ... - * # TYPE sale_buyer_count gauge - * sale_buyer_count 33 1677707139456 - * # HELP sale_cf_participants_count - * ... - */ -export const parseSnsSwapSaleBuyerCount = ( - text: string -): number | undefined => { - const value = Number( - text - .split("\n") - ?.find((line) => line.startsWith("sale_buyer_count ")) - ?.split(/\s/)?.[1] - ); - return isNaN(value) ? undefined : value; -}; - /** * An SNS is in finalization state if: * diff --git a/frontend/src/tests/e2e/sns-swap-participant-count.spec.ts b/frontend/src/tests/e2e/sns-swap-participant-count.spec.ts new file mode 100644 index 00000000000..dba474acc03 --- /dev/null +++ b/frontend/src/tests/e2e/sns-swap-participant-count.spec.ts @@ -0,0 +1,83 @@ +import { AppPo } from "$tests/page-objects/App.page-object"; +import { ProjectCommitmentPo } from "$tests/page-objects/ProjectCommitment.page-object"; +import { PlaywrightPageObjectElement } from "$tests/page-objects/playwright.page-object"; +import { + disableCssAnimations, + signInWithNewUser, + step, +} from "$tests/utils/e2e.test-utils"; +import { expect, test } from "@playwright/test"; + +// The swap participant count must come from the certified `get_derived_state` +// call. The removed code read it from `https://.raw.icp0.io/metrics`, +// which the raw gateway serves without response certification. +const RAW_METRICS_PATTERN = /\.raw\.(icp0\.io|ic0\.app)\/metrics/; + +// playwright.config.ts sets expect.timeout to 0, so every poll needs its own. +const POLL_TIMEOUT = 60_000; + +test("Test SNS swap participant count", async ({ page, context }) => { + const requestedUrls: string[] = []; + page.on("request", (request) => requestedUrls.push(request.url())); + + const rawMetricsRequests = () => + requestedUrls.filter((url) => RAW_METRICS_PATTERN.test(url)); + + await page.goto("/"); + await disableCssAnimations(page); + + const pageElement = PlaywrightPageObjectElement.fromPage(page); + const appPo = new AppPo(pageElement); + const projectCommitmentPo = ProjectCommitmentPo.under(pageElement); + const projectDetail = appPo.getProjectDetailPo(); + + await step("Open the detail page of a sale that accepts participation"); + await appPo.goToLaunchpad(); + await appPo.getLaunchpad2Po().getUpcomingLaunchesCardListPo().waitFor(); + const upcomingLaunchesCards = await appPo + .getLaunchpad2Po() + .getUpcomingLaunchesCardListPo() + .getCardEntries(); + await upcomingLaunchesCards[0].click(); + + await projectDetail.waitForContentLoaded(); + expect(await projectDetail.getStatus()).toBe("Accepting Participation"); + + await step("The page shows a participant count"); + await expect + .poll(() => projectCommitmentPo.hasParticipantsCount(), { + timeout: POLL_TIMEOUT, + }) + .toBe(true); + const countBeforeParticipation = + await projectCommitmentPo.getParticipantsCount(); + expect(Number.isNaN(countBeforeParticipation)).toBe(false); + + await step("The page requests no metrics from the raw domain"); + expect(rawMetricsRequests()).toEqual([]); + + await step("Sign in and get some ICP to participate in the sale"); + await signInWithNewUser({ page, context }); + await appPo.goBack(); + await appPo.getIcpTokens(20); + await upcomingLaunchesCards[0].click(); + await projectDetail.waitForContentLoaded(); + + await step("Participate in the sale"); + expect(await projectDetail.hasCommitmentAmount()).toBe(false); + await projectDetail.participate({ amount: 5, acceptConditions: true }); + expect(await projectDetail.getCommitmentAmount()).toBe("5.00"); + + await step("The participant count rises, so it follows the swap state"); + // Another worker can also participate in this sale at the same time + // (sns-participation.spec.ts uses the same first upcoming launch), so the + // count can rise by more than one. Check for at least one, not exactly one. + await expect + .poll(() => projectCommitmentPo.getParticipantsCount(), { + timeout: POLL_TIMEOUT, + }) + .toBeGreaterThanOrEqual(countBeforeParticipation + 1); + + await step("No request went to the raw metrics domain at any point"); + expect(rawMetricsRequests()).toEqual([]); +}); diff --git a/frontend/src/tests/lib/api/sns-swap-metrics.api.spec.ts b/frontend/src/tests/lib/api/sns-swap-metrics.api.spec.ts deleted file mode 100644 index 561feee2bb9..00000000000 --- a/frontend/src/tests/lib/api/sns-swap-metrics.api.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { querySnsSwapMetrics } from "$lib/api/sns-swap-metrics.api"; -import { mockPrincipal } from "$tests/mocks/auth.store.mock"; - -describe("sns-swap-metrics.api", () => { - it("should query raw metrics with swapCanisterId", async () => { - const mockFetch = vi.fn(); - mockFetch.mockReturnValueOnce( - Promise.resolve({ - ok: true, - text: () => Promise.resolve("test"), - }) - ); - global.fetch = mockFetch; - - const swapCanisterId = mockPrincipal; - const result = await querySnsSwapMetrics({ swapCanisterId }); - - expect(result).toEqual("test"); - expect(mockFetch).toBeCalledTimes(1); - expect(mockFetch).toBeCalledWith( - expect.stringContaining(swapCanisterId.toText()) - ); - }); -}); diff --git a/frontend/src/tests/lib/components/project-detail/ProjectCommitment.spec.ts b/frontend/src/tests/lib/components/project-detail/ProjectCommitment.spec.ts index 85a7af7b773..5835608fe9d 100644 --- a/frontend/src/tests/lib/components/project-detail/ProjectCommitment.spec.ts +++ b/frontend/src/tests/lib/components/project-detail/ProjectCommitment.spec.ts @@ -1,5 +1,4 @@ import ProjectCommitment from "$lib/components/project-detail/ProjectCommitment.svelte"; -import { snsSwapMetricsStore } from "$lib/stores/sns-swap-metrics.store"; import type { SnsSwapCommitment } from "$lib/types/sns"; import type { SnsSummaryWrapper } from "$lib/types/sns-summary-wrapper"; import { @@ -37,29 +36,35 @@ describe("ProjectCommitment", () => { expect(await po.getMinCommitment()).toEqual("200.00 ICP"); }); - it("should render total participants from swap metrics", async () => { - snsSwapMetricsStore.setMetrics({ - rootCanisterId: mockSnsFullProject.swapCommitment.rootCanisterId, - metrics: { - saleBuyerCount, - }, - }); + it("should not render total participants when the derived state has none", async () => { const summaryWithoutBuyers = createSummary({ lifecycle: SnsSwapLifecycle.Open, buyersCount: null, }); const po = renderComponent(summaryWithoutBuyers); - expect(await po.getParticipantsCount()).toEqual(saleBuyerCount); + expect(await po.hasParticipantsCount()).toBe(false); }); - it("should render total participants from derived state", async () => { - snsSwapMetricsStore.setMetrics({ - rootCanisterId: mockSnsFullProject.swapCommitment.rootCanisterId, - metrics: { - saleBuyerCount: 0, - }, + it("should not render success message when the derived state has no participants count", async () => { + const directCommitment = 30000000000n; + const summary = createSummary({ + lifecycle: SnsSwapLifecycle.Open, + currentTotalCommitment: directCommitment, + neuronsFundCommitment: 0n, + directCommitment, + minDirectParticipation: 10000000000n, + maxDirectParticipation: 100000000000n, + buyersCount: null, + minParticipants: 100, }); + const po = renderComponent(summary); + + expect(await po.hasParticipantsCount()).toBe(false); + expect(await po.getGoalReachedMessage()).toEqual(null); + }); + + it("should render total participants from derived state", async () => { const summaryWithBuyersCount = createSummary({ lifecycle: SnsSwapLifecycle.Open, buyersCount: BigInt(saleBuyerCount), diff --git a/frontend/src/tests/lib/pages/ProjectDetail.spec.ts b/frontend/src/tests/lib/pages/ProjectDetail.spec.ts index e9c777f237e..e9edf0e904b 100644 --- a/frontend/src/tests/lib/pages/ProjectDetail.spec.ts +++ b/frontend/src/tests/lib/pages/ProjectDetail.spec.ts @@ -3,7 +3,6 @@ import * as locationApi from "$lib/api/location.api"; import * as nnsDappApi from "$lib/api/nns-dapp.api"; import * as proposalsApi from "$lib/api/proposals.api"; import * as snsSaleApi from "$lib/api/sns-sale.api"; -import * as snsMetricsApi from "$lib/api/sns-swap-metrics.api"; import * as snsApi from "$lib/api/sns.api"; import { SECONDS_IN_DAY } from "$lib/constants/constants"; import { AppPath } from "$lib/constants/routes.constants"; @@ -64,14 +63,6 @@ vi.mock("$lib/api/sns.api", async (importOriginal) => { }; }); -vi.mock("$lib/api/sns-swap-metrics.api", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - }; -}); - vi.mock("$lib/api/sns-sale.api", async (importOriginal) => { const actual = await importOriginal(); return { @@ -110,11 +101,6 @@ describe("ProjectDetail", () => { const userCountryCode = "CH"; const notUserCountryCode = "US"; const newBalance = 10_000_000_000n; - const saleBuyerCount = 1_000_000; - const rawMetricsText = ` -# TYPE sale_buyer_count gauge -sale_buyer_count ${saleBuyerCount} 1677707139456 -# HELP sale_cf_participants_count`; const now = Date.now(); const nowInSeconds = Math.floor(now / 1000); @@ -145,10 +131,6 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 vi.spyOn(snsSaleApi, "queryFinalizationStatus").mockResolvedValue( snsFinalizationStatusResponseMock ); - - vi.spyOn(snsMetricsApi, "querySnsSwapMetrics").mockResolvedValue( - rawMetricsText - ); }); const renderComponent = ({ @@ -177,7 +159,6 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 setNoIdentity(); }); - // TODO: Remove once all SNSes support buyers count in derived state describe("Open project without buyers count on derived state", () => { const props = { rootCanisterId: rootCanisterId.toText(), @@ -194,15 +175,23 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 ]); }); - it("should fetch swap metrics on load", async () => { - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(0); + it("should not fetch the swap metrics from the raw domain", async () => { renderComponent(props); await runResolvedPromises(); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledWith({ - swapCanisterId, - }); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(1); + + const fetchedUrls = vi + .mocked(global.fetch) + .mock.calls.map(([url]) => String(url)); + expect( + fetchedUrls.filter((url) => url.includes("raw.icp0.io")) + ).toEqual([]); + }); + + it("should render status section", async () => { + const po = renderComponent(props); + + expect(await po.getProjectStatusSectionPo().isPresent()).toBe(true); }); }); @@ -221,19 +210,6 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 ]); }); - it("should NOT start watching swap metrics", async () => { - renderComponent(props); - - await runResolvedPromises(); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(0); - - const retryDelay = WATCH_SALE_STATE_EVERY_MILLISECONDS; - await advanceTime(retryDelay); - await runResolvedPromises(); - - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(0); - }); - it("should start watching derived state and stop on unmounting", async () => { let unmount: () => void; const unmountWhen = new Promise((resolve) => { @@ -287,38 +263,6 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 }); }); - // TODO: Remove once all SNSes support buyers count in derived state - describe("Committed project without buyers in derived state", () => { - const props = { - rootCanisterId: rootCanisterId.toText(), - }; - beforeEach(() => { - setSnsProjects([ - { - rootCanisterId, - lifecycle: SnsSwapLifecycle.Committed, - directParticipantCount: [], - certified: true, - }, - ]); - }); - - it("should query metrics but not watch them", async () => { - const po = renderComponent(props); - - expect(await po.getProjectStatusSectionPo().isPresent()).toBe(true); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(1); - - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(1); - - const retryDelay = WATCH_SALE_STATE_EVERY_MILLISECONDS; - - // Even after waiting a long time there shouldn't be more calls. - await advanceTime(99 * retryDelay); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(1); - }); - }); - describe("Committed project with buyers count in derived state", () => { const props = { rootCanisterId: rootCanisterId.toText(), @@ -335,20 +279,6 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 ]); }); - it("should NOT query metrics nor watch them", async () => { - const po = renderComponent(props); - - expect(await po.getProjectStatusSectionPo().isPresent()).toBe(true); - - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(0); - - const retryDelay = WATCH_SALE_STATE_EVERY_MILLISECONDS; - - // Even after waiting a long time there shouldn't be more calls. - await advanceTime(99 * retryDelay); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(0); - }); - it("should not query total commitments, nor start watching them", async () => { const po = renderComponent(props); @@ -752,43 +682,6 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 }); }); - describe("Committed project", () => { - const props = { - rootCanisterId: rootCanisterId.toText(), - }; - beforeEach(() => { - setSnsProjects([ - { - rootCanisterId, - lifecycle: SnsSwapLifecycle.Committed, - directParticipantCount: [], - certified: true, - }, - ]); - vi.spyOn(snsApi, "querySnsSwapCommitment").mockResolvedValue({ - rootCanisterId, - myCommitment: { - icp: [], - has_created_neuron_recipes: [], - }, - }); - }); - - it("should query metrics but not watch them", async () => { - const po = renderComponent(props); - - expect(await po.getProjectStatusSectionPo().isPresent()).toBe(true); - - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(1); - - const retryDelay = WATCH_SALE_STATE_EVERY_MILLISECONDS; - - // Even after waiting a long time there shouldn't be more calls. - await advanceTime(99 * retryDelay); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(1); - }); - }); - describe("Committed project with buyers count in state", () => { const props = { rootCanisterId: rootCanisterId.toText(), @@ -820,20 +713,6 @@ sale_buyer_count ${saleBuyerCount} 1677707139456 }); }); - it("should NOT query metrics nor watch them", async () => { - const po = renderComponent(props); - - expect(await po.getProjectStatusSectionPo().isPresent()).toBe(true); - - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(0); - - const retryDelay = WATCH_SALE_STATE_EVERY_MILLISECONDS; - - // Even after waiting a long time there shouldn't be more calls. - await advanceTime(99 * retryDelay); - expect(snsMetricsApi.querySnsSwapMetrics).toBeCalledTimes(0); - }); - it("should not query total commitments, nor start watching them", async () => { const po = renderComponent(props); diff --git a/frontend/src/tests/lib/services/sns-swap-metrics.services.spec.ts b/frontend/src/tests/lib/services/sns-swap-metrics.services.spec.ts deleted file mode 100644 index b255bd0965d..00000000000 --- a/frontend/src/tests/lib/services/sns-swap-metrics.services.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as snsSwapMetrics from "$lib/api/sns-swap-metrics.api"; -import { loadSnsSwapMetrics } from "$lib/services/sns-swap-metrics.services"; -import { snsSwapMetricsStore } from "$lib/stores/sns-swap-metrics.store"; -import { mockPrincipal } from "$tests/mocks/auth.store.mock"; -import { Principal } from "@icp-sdk/core/principal"; -import { get } from "svelte/store"; - -describe("sns-swap-metrics", () => { - describe("loadSnsSwapMetrics", () => { - const rootCanisterId = mockPrincipal; - const swapCanisterId = Principal.fromText("aaaaa-aa"); - const saleBuyerCount = 1_000_000; - const rawMetricsText = ` -# TYPE sale_buyer_count gauge -sale_buyer_count ${saleBuyerCount} 1677707139456 -# HELP sale_cf_participants_count`; - - it("should call querySnsSwapMetrics api and load metrics in the store", async () => { - const querySnsSwapMetricsSpy = vi - .spyOn(snsSwapMetrics, "querySnsSwapMetrics") - .mockResolvedValue(rawMetricsText); - await loadSnsSwapMetrics({ - rootCanisterId, - swapCanisterId, - forceFetch: false, - }); - - expect(querySnsSwapMetricsSpy).toBeCalledTimes(1); - expect(querySnsSwapMetricsSpy).toBeCalledWith({ - swapCanisterId, - }); - expect( - get(snsSwapMetricsStore)[rootCanisterId.toText()]?.saleBuyerCount - ).toEqual(saleBuyerCount); - }); - - it("should skip querySnsSwapMetrics call when metrics available in store", async () => { - snsSwapMetricsStore.setMetrics({ - rootCanisterId, - metrics: { saleBuyerCount: 123 }, - }); - - const querySnsSwapMetricsSpy = vi - .spyOn(snsSwapMetrics, "querySnsSwapMetrics") - .mockResolvedValue(rawMetricsText); - await loadSnsSwapMetrics({ - rootCanisterId, - swapCanisterId, - forceFetch: false, - }); - - expect(querySnsSwapMetricsSpy).not.toBeCalled(); - }); - - it("should respect forceFetch flag", async () => { - snsSwapMetricsStore.setMetrics({ - rootCanisterId, - metrics: { saleBuyerCount: 123 }, - }); - const querySnsSwapMetricsSpy = vi - .spyOn(snsSwapMetrics, "querySnsSwapMetrics") - .mockResolvedValue(rawMetricsText); - - expect( - get(snsSwapMetricsStore)[rootCanisterId.toText()]?.saleBuyerCount - ).not.toEqual(saleBuyerCount); - - await loadSnsSwapMetrics({ - rootCanisterId, - swapCanisterId, - forceFetch: true, - }); - - expect(querySnsSwapMetricsSpy).toBeCalledTimes(1); - expect( - get(snsSwapMetricsStore)[rootCanisterId.toText()]?.saleBuyerCount - ).toEqual(saleBuyerCount); - }); - }); -}); diff --git a/frontend/src/tests/lib/stores/sns-swap-metrics.store.spec.ts b/frontend/src/tests/lib/stores/sns-swap-metrics.store.spec.ts deleted file mode 100644 index 3c559dd393b..00000000000 --- a/frontend/src/tests/lib/stores/sns-swap-metrics.store.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { snsSwapMetricsStore } from "$lib/stores/sns-swap-metrics.store"; -import { mockPrincipal } from "$tests/mocks/auth.store.mock"; -import { get } from "svelte/store"; - -describe("snsSwapMetricsStore", () => { - const metrics = { - saleBuyerCount: 123, - }; - - it("should set metrics", () => { - snsSwapMetricsStore.setMetrics({ - rootCanisterId: mockPrincipal, - metrics, - }); - - const $snsSwapMetricsStore = get(snsSwapMetricsStore); - expect($snsSwapMetricsStore[mockPrincipal.toText()]).toEqual(metrics); - }); -}); diff --git a/frontend/src/tests/lib/utils/sns-swap.utils.spec.ts b/frontend/src/tests/lib/utils/sns-swap.utils.spec.ts index 1d8633c6a74..295c550b16b 100644 --- a/frontend/src/tests/lib/utils/sns-swap.utils.spec.ts +++ b/frontend/src/tests/lib/utils/sns-swap.utils.spec.ts @@ -1,120 +1,34 @@ -import { hasBuyersCount, swapSaleBuyerCount } from "$lib/utils/sns-swap.utils"; -import { mockPrincipal } from "$tests/mocks/auth.store.mock"; +import { swapSaleBuyerCount } from "$lib/utils/sns-swap.utils"; import { mockDerived } from "$tests/mocks/sns-projects.mock"; import type { SnsSwapDid } from "@icp-sdk/canisters/sns"; -import { Principal } from "@icp-sdk/core/principal"; describe("sns-swap utils", () => { describe("swapSaleBuyerCount", () => { - describe("derived state does NOT have buyers count", () => { + it("should return undefined when the derived state has no participant count", () => { const derivedState: SnsSwapDid.DerivedState = { ...mockDerived, direct_participant_count: [], }; - it("should return undefined if rootCanisterId or metrics are not defined", () => { - expect( - swapSaleBuyerCount({ - rootCanisterId: undefined, - swapMetrics: undefined, - derivedState, - }) - ).toBeUndefined(); - expect( - swapSaleBuyerCount({ - rootCanisterId: undefined, - swapMetrics: null, - derivedState, - }) - ).toBeUndefined(); - expect( - swapSaleBuyerCount({ - rootCanisterId: mockPrincipal, - swapMetrics: undefined, - derivedState, - }) - ).toBeUndefined(); - expect( - swapSaleBuyerCount({ - rootCanisterId: mockPrincipal, - swapMetrics: null, - derivedState, - }) - ).toBeUndefined(); - expect( - swapSaleBuyerCount({ - rootCanisterId: undefined, - swapMetrics: { ["aaaaa-aa"]: { saleBuyerCount: 10 } }, - derivedState, - }) - ).toBeUndefined(); - }); - it("should return undefined if root canister id is not in the metrics store", () => { - expect( - swapSaleBuyerCount({ - rootCanisterId: Principal.fromText( - "xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe" - ), - swapMetrics: { ["aaaaa-aa"]: { saleBuyerCount: 10 } }, - derivedState, - }) - ).toBeUndefined(); - }); - - it("should return sale count of the selected root canister id", () => { - const saleCount = 100; - expect( - swapSaleBuyerCount({ - rootCanisterId: mockPrincipal, - swapMetrics: { - [mockPrincipal.toText()]: { saleBuyerCount: saleCount }, - }, - derivedState, - }) - ).toBe(saleCount); - }); + expect(swapSaleBuyerCount({ derivedState })).toBeUndefined(); }); - describe("derived state has buyers count", () => { - const participantsCount = 100; + it("should return the participant count of the derived state", () => { const derivedState: SnsSwapDid.DerivedState = { ...mockDerived, - direct_participant_count: [BigInt(participantsCount)], + direct_participant_count: [30n], }; - it("should return sale count of the derived store ignoring the swap metrics data", () => { - expect( - swapSaleBuyerCount({ - rootCanisterId: mockPrincipal, - swapMetrics: { - [mockPrincipal.toText()]: { saleBuyerCount: 400 }, - }, - derivedState, - }) - ).toBe(participantsCount); - }); - }); - }); - describe("hasBuyersCount", () => { - const derivedWithBuyersCount: SnsSwapDid.DerivedState = { - ...mockDerived, - direct_participant_count: [100n], - }; - const derivedWithoutBuyersCount: SnsSwapDid.DerivedState = { - ...mockDerived, - direct_participant_count: [], - }; - it("should return undefined if derived state is undefined or null", () => { - expect(hasBuyersCount(undefined)).toBeUndefined(); - expect(hasBuyersCount(null)).toBeUndefined(); + expect(swapSaleBuyerCount({ derivedState })).toBe(30); }); - it("should return true if derived state has buyers count", () => { - expect(hasBuyersCount(derivedWithBuyersCount)).toBe(true); - }); + it("should return zero when the derived state reports no participant", () => { + const derivedState: SnsSwapDid.DerivedState = { + ...mockDerived, + direct_participant_count: [0n], + }; - it("should return false if derived state has no buyers count", () => { - expect(hasBuyersCount(derivedWithoutBuyersCount)).toBe(false); + expect(swapSaleBuyerCount({ derivedState })).toBe(0); }); }); }); diff --git a/frontend/src/tests/lib/utils/sns.utils.spec.ts b/frontend/src/tests/lib/utils/sns.utils.spec.ts index c8a950a8067..e6969b35998 100644 --- a/frontend/src/tests/lib/utils/sns.utils.spec.ts +++ b/frontend/src/tests/lib/utils/sns.utils.spec.ts @@ -13,7 +13,6 @@ import { isSnsGenericNervousSystemTypeProposal, isSnsLedgerCanisterId, isSnsNativeNervousSystemFunction, - parseSnsSwapSaleBuyerCount, swapEndedMoreThanOneWeekAgo, } from "$lib/utils/sns.utils"; import { mockIdentity, mockPrincipal } from "$tests/mocks/auth.store.mock"; @@ -192,41 +191,6 @@ describe("sns-utils", () => { }); }); - describe("parseSnsSwapSaleBuyerCount", () => { - const saleBuyerCount = 1_000_000; - const RAW_METRICS = ` -# TYPE sale_buyer_count gauge -sale_buyer_count ${saleBuyerCount} 1677707139456 -# HELP sale_cf_participants_count`; - - it("returns sale_buyer_count value", () => { - expect(parseSnsSwapSaleBuyerCount(RAW_METRICS)).toEqual(saleBuyerCount); - }); - - it("returns undefined when sale_buyer_count not found", () => { - const WRONG_METRICS = ` -# TYPE sale_buyer_count gauge -sale_participants_count ${saleBuyerCount} 1677707139456 -# HELP sale_cf_participants_count`; - expect(parseSnsSwapSaleBuyerCount(WRONG_METRICS)).toBeUndefined(); - }); - - it("returns false on unknown error", () => { - const error = new Error("Fake the swap has already reached its target"); - expect(isInternalRefreshBuyerTokensError(error)).toBe(false); - }); - - it("returns false on not error argument", () => { - expect(isInternalRefreshBuyerTokensError(null)).toBe(false); - expect(isInternalRefreshBuyerTokensError(undefined)).toBe(false); - expect( - isInternalRefreshBuyerTokensError( - "The swap has already reached its target" - ) - ).toBe(false); - }); - }); - describe("isSnsFinalizing", () => { it("returns true if finalizing", () => { const finalizingResponse = createFinalizationStatusMock(true); diff --git a/frontend/src/tests/page-objects/ProjectCommitment.page-object.ts b/frontend/src/tests/page-objects/ProjectCommitment.page-object.ts index 665276d1729..41e66d44f45 100644 --- a/frontend/src/tests/page-objects/ProjectCommitment.page-object.ts +++ b/frontend/src/tests/page-objects/ProjectCommitment.page-object.ts @@ -31,7 +31,14 @@ export class ProjectCommitmentPo extends BasePageObject { return Number(await this.getText("sns-project-current-sale-buyer-count")); } - async getGoalReachedMessage(): Promise { + hasParticipantsCount(): Promise { + return this.isPresent("sns-project-current-sale-buyer-count"); + } + + async getGoalReachedMessage(): Promise { + if (!(await this.isPresent("min-participation-reached"))) { + return null; + } return this.getText("min-participation-reached"); }