Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions CHANGELOG-Nns-Dapp-unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 0 additions & 28 deletions frontend/src/lib/api/sns-swap-metrics.api.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,8 +53,6 @@

let saleBuyerCount: number | undefined;
$: saleBuyerCount = swapSaleBuyerCount({
rootCanisterId: $projectDetailStore?.summary?.rootCanisterId,
swapMetrics: $snsSwapMetricsStore,
derivedState: summary.derived,
});

Expand Down
16 changes: 0 additions & 16 deletions frontend/src/lib/pages/ProjectDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
hidePollingToast,
restoreSnsSaleParticipation,
} from "$lib/services/sns-sale.services";
import { loadSnsSwapMetrics } from "$lib/services/sns-swap-metrics.services";
import {
loadSnsDerivedState,
loadSnsLifecycle,
Expand All @@ -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";
Expand Down Expand Up @@ -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?.();
Expand Down
49 changes: 0 additions & 49 deletions frontend/src/lib/services/sns-swap-metrics.services.ts

This file was deleted.

52 changes: 0 additions & 52 deletions frontend/src/lib/stores/sns-swap-metrics.store.ts

This file was deleted.

37 changes: 8 additions & 29 deletions frontend/src/lib/utils/sns-swap.utils.ts
Original file line number Diff line number Diff line change
@@ -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;
};
22 changes: 0 additions & 22 deletions frontend/src/lib/utils/sns.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
Expand Down
82 changes: 82 additions & 0 deletions frontend/src/tests/e2e/sns-swap-participant-count.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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://<swap>.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 by one, so it follows the swap state"
);
await expect
.poll(() => projectCommitmentPo.getParticipantsCount(), {
timeout: POLL_TIMEOUT,
})
.toBe(countBeforeParticipation + 1);

await step("No request went to the raw metrics domain at any point");
expect(rawMetricsRequests()).toEqual([]);
});
24 changes: 0 additions & 24 deletions frontend/src/tests/lib/api/sns-swap-metrics.api.spec.ts

This file was deleted.

Loading
Loading