Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
83 changes: 83 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,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://<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, 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([]);
});
24 changes: 0 additions & 24 deletions frontend/src/tests/lib/api/sns-swap-metrics.api.spec.ts

This file was deleted.

Loading
Loading