Skip to content

Commit d8489e8

Browse files
authored
feat(launchpad2): sns project sorting (#7131)
# Motivation Projects on the launchpad should be ordered based on the following criteria: 1. Highest proposal activity 2. Largest market cap 3. Most ICP treasury left 4. Any project with `N/A` in any of these fields should always be placed at the end **Out of scope:** updated sorting for committed projects. # Changes - Added new comparators: - `compareLaunchpadSnsProjects` - `compareSnsProjectsUndefinedProposalActivityLast` - `compareSnsProjectsUndefinedIcpTreasuryLast` - `compareSnsProjectsUndefinedMarketCapLast` - `compareSnsProjectsByProposalActivity` - `compareSnsProjectsByMarketCap` - `compareSnsProjectsByIcpTreasury` - Applied `compareLaunchpadSnsProjects` to sort project cards # Tests - Added unit tests - Verified against mainnet — ordering matches expectations <img width="1287" height="692" alt="image" src="https://github.com/user-attachments/assets/848dd1bd-152f-4c42-aa69-f9d0cec84c18" /> # Todos - [ ] Accessibility (a11y) – any impact? - [ ] Changelog – is it needed?
1 parent 5f5a852 commit d8489e8

File tree

6 files changed

+709
-27
lines changed

6 files changed

+709
-27
lines changed

frontend/src/lib/pages/Launchpad2.svelte

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
import CardList from "$lib/components/launchpad/CardList.svelte";
33
import ProjectCard2 from "$lib/components/launchpad/ProjectCard2.svelte";
44
import SkeletonProjectCard from "$lib/components/launchpad/SkeletonProjectCard.svelte";
5+
import { icpSwapUsdPricesStore } from "$lib/derived/icp-swap.derived";
56
import type { SnsFullProject } from "$lib/derived/sns/sns-projects.derived";
7+
import { snsTotalSupplyTokenAmountStore } from "$lib/derived/sns/sns-total-supply-token-amount.derived";
68
import { isMobileViewportStore } from "$lib/derived/viewport.derived";
79
import { i18n } from "$lib/stores/i18n";
810
import type { ComponentWithProps } from "$lib/types/svelte";
9-
import { getUpcomingLaunchesCards } from "$lib/utils/launchpad.utils";
11+
import {
12+
compareLaunchpadSnsProjects,
13+
getUpcomingLaunchesCards,
14+
} from "$lib/utils/launchpad.utils";
1015
import {
1116
comparesByDecentralizationSaleOpenTimestampDesc,
1217
filterProjectsStatus,
@@ -42,6 +47,12 @@
4247
.filter(
4348
({ swapCommitment }) => getCommitmentE8s(swapCommitment) ?? 0n > 0n
4449
)
50+
.sort(
51+
compareLaunchpadSnsProjects({
52+
snsTotalSupplyTokenAmountStore: $snsTotalSupplyTokenAmountStore,
53+
icpSwapUsdPricesStore: $icpSwapUsdPricesStore,
54+
})
55+
)
4556
.map((project) => ({
4657
Component: ProjectCard2 as unknown as Component,
4758
props: { project },
@@ -54,6 +65,12 @@
5465
isNullish(getCommitmentE8s(swapCommitment)) ||
5566
getCommitmentE8s(swapCommitment) === 0n
5667
)
68+
.sort(
69+
compareLaunchpadSnsProjects({
70+
snsTotalSupplyTokenAmountStore: $snsTotalSupplyTokenAmountStore,
71+
icpSwapUsdPricesStore: $icpSwapUsdPricesStore,
72+
})
73+
)
5774
.map((project) => ({
5875
Component: ProjectCard2 as unknown as Component,
5976
props: { project },

frontend/src/lib/utils/launchpad.utils.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import CreateSnsProposalCard from "$lib/components/launchpad/CreateSnsProposalCard.svelte";
22
import OngoingProjectCard from "$lib/components/launchpad/OngoingProjectCard.svelte";
33
import UpcomingProjectCard from "$lib/components/launchpad/UpcomingProjectCard.svelte";
4+
import { abandonedProjectsCanisterId } from "$lib/constants/canister-ids.constants";
5+
import type { IcpSwapUsdPricesStoreData } from "$lib/derived/icp-swap.derived";
46
import type { SnsFullProject } from "$lib/derived/sns/sns-projects.derived";
57
import type { ComponentWithProps } from "$lib/types/svelte";
68
import { compareProposalInfoByDeadlineTimestampSeconds } from "$lib/utils/portfolio.utils";
79
import {
810
comparesByDecentralizationSaleOpenTimestampDesc,
911
filterProjectsStatus,
12+
snsProjectIcpInTreasuryPercentage,
13+
snsProjectMarketCap,
14+
snsProjectWeeklyProposalActivity,
1015
} from "$lib/utils/projects.utils";
16+
import {
17+
createAscendingComparator,
18+
createDescendingComparator,
19+
mergeComparators,
20+
} from "$lib/utils/sort.utils";
1121
import type { ProposalInfo } from "@dfinity/nns";
1222
import { SnsSwapLifecycle } from "@dfinity/sns";
23+
import { TokenAmountV2 } from "@dfinity/utils";
1324
import type { Component } from "svelte";
1425

1526
export const getUpcomingLaunchesCards = ({
@@ -54,3 +65,82 @@ export const getUpcomingLaunchesCards = ({
5465
...adoptedSnsProposalCards,
5566
];
5667
};
68+
69+
export const compareSnsProjectsAbandonedLast = createAscendingComparator(
70+
(project: SnsFullProject) =>
71+
abandonedProjectsCanisterId.includes(project.rootCanisterId.toText())
72+
);
73+
74+
export const compareSnsProjectsUndefinedIcpTreasuryLast =
75+
createAscendingComparator(
76+
(project: SnsFullProject) =>
77+
snsProjectIcpInTreasuryPercentage(project) === undefined
78+
);
79+
80+
export const compareSnsProjectsUndefinedProposalActivityLast =
81+
createAscendingComparator(
82+
(project: SnsFullProject) =>
83+
snsProjectWeeklyProposalActivity(project) === undefined
84+
);
85+
86+
export const compareSnsProjectsUndefinedMarketCapLast = ({
87+
snsTotalSupplyTokenAmountStore,
88+
icpSwapUsdPricesStore,
89+
}: {
90+
snsTotalSupplyTokenAmountStore: Record<string, TokenAmountV2>;
91+
icpSwapUsdPricesStore: IcpSwapUsdPricesStoreData;
92+
}) =>
93+
createAscendingComparator(
94+
(project: SnsFullProject) =>
95+
snsProjectMarketCap({
96+
sns: project,
97+
snsTotalSupplyTokenAmountStore,
98+
icpSwapUsdPricesStore,
99+
}) === undefined
100+
);
101+
102+
export const compareSnsProjectsByProposalActivity = createDescendingComparator(
103+
(project: SnsFullProject) => snsProjectWeeklyProposalActivity(project)
104+
);
105+
106+
export const compareSnsProjectsByMarketCap = ({
107+
snsTotalSupplyTokenAmountStore,
108+
icpSwapUsdPricesStore,
109+
}: {
110+
snsTotalSupplyTokenAmountStore: Record<string, TokenAmountV2>;
111+
icpSwapUsdPricesStore: IcpSwapUsdPricesStoreData;
112+
}) =>
113+
createDescendingComparator(
114+
(project: SnsFullProject) =>
115+
snsProjectMarketCap({
116+
sns: project,
117+
snsTotalSupplyTokenAmountStore,
118+
icpSwapUsdPricesStore,
119+
}) ?? 0n
120+
);
121+
export const compareSnsProjectsByIcpTreasury = createDescendingComparator(
122+
(project: SnsFullProject) => snsProjectIcpInTreasuryPercentage(project)
123+
);
124+
125+
export const compareLaunchpadSnsProjects = ({
126+
snsTotalSupplyTokenAmountStore,
127+
icpSwapUsdPricesStore,
128+
}: {
129+
snsTotalSupplyTokenAmountStore: Record<string, TokenAmountV2>;
130+
icpSwapUsdPricesStore: IcpSwapUsdPricesStoreData;
131+
}) =>
132+
mergeComparators([
133+
compareSnsProjectsAbandonedLast,
134+
compareSnsProjectsUndefinedProposalActivityLast,
135+
compareSnsProjectsUndefinedIcpTreasuryLast,
136+
compareSnsProjectsUndefinedMarketCapLast({
137+
snsTotalSupplyTokenAmountStore,
138+
icpSwapUsdPricesStore,
139+
}),
140+
compareSnsProjectsByProposalActivity,
141+
compareSnsProjectsByMarketCap({
142+
snsTotalSupplyTokenAmountStore,
143+
icpSwapUsdPricesStore,
144+
}),
145+
compareSnsProjectsByIcpTreasury,
146+
]);

frontend/src/lib/utils/projects.utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,10 @@ export const snsProjectMarketCap = ({
517517
snsTotalSupplyTokenAmountStore: Record<string, TokenAmountV2>;
518518
icpSwapUsdPricesStore: IcpSwapUsdPricesStoreData;
519519
}): number | undefined => {
520-
const { rootCanisterId, ledgerCanisterId } = sns.summary;
520+
const {
521+
rootCanisterId,
522+
summary: { ledgerCanisterId },
523+
} = sns;
521524
const totalSupplyE8s =
522525
snsTotalSupplyTokenAmountStore[rootCanisterId.toText()]?.toE8s();
523526
const totalSupply = nonNullish(totalSupplyE8s)

frontend/src/tests/lib/pages/Launchpad2.spec.ts

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "$tests/mocks/proposal.mock";
77
import {
88
createMockSnsFullProject,
9+
mockSnsMetrics,
910
principal,
1011
} from "$tests/mocks/sns-projects.mock";
1112
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
@@ -89,26 +90,68 @@ describe("Launchpad2", () => {
8990
});
9091

9192
it("displays launched cards in correct order", async () => {
93+
const project0 = createMockSnsFullProject({
94+
rootCanisterId: principal(1),
95+
summaryParams: {
96+
lifecycle: SnsSwapLifecycle.Committed,
97+
swapOpenTimestampSeconds: BigInt(168_000_000),
98+
projectName: "Outdated Project",
99+
},
100+
metrics: undefined,
101+
});
92102
const project1 = createMockSnsFullProject({
93103
rootCanisterId: principal(1),
94104
summaryParams: {
95105
lifecycle: SnsSwapLifecycle.Committed,
96106
swapOpenTimestampSeconds: BigInt(168_000_000),
97-
projectName: "Not Committed Project",
107+
projectName: "Not Committed, Not Active Project",
108+
},
109+
metrics: {
110+
...mockSnsMetrics,
111+
num_recently_executed_proposals: 0,
112+
},
113+
});
114+
const project12 = createMockSnsFullProject({
115+
rootCanisterId: principal(1),
116+
summaryParams: {
117+
lifecycle: SnsSwapLifecycle.Committed,
118+
swapOpenTimestampSeconds: BigInt(168_000_000),
119+
projectName: "Not Committed, Very Active Project",
120+
},
121+
metrics: {
122+
...mockSnsMetrics,
123+
num_recently_executed_proposals: 22,
98124
},
99125
});
100-
const project2 = createMockSnsFullProject({
126+
const project3 = createMockSnsFullProject({
101127
rootCanisterId: principal(1),
102128
summaryParams: {
103129
lifecycle: SnsSwapLifecycle.Committed,
104130
swapOpenTimestampSeconds: BigInt(168_000_000),
105-
projectName: "Committed Project",
131+
projectName: "Committed, Not Active Project",
106132
},
107133
icpCommitment: 10_000_000n,
134+
metrics: {
135+
...mockSnsMetrics,
136+
num_recently_executed_proposals: 0,
137+
},
138+
});
139+
const project4 = createMockSnsFullProject({
140+
rootCanisterId: principal(1),
141+
summaryParams: {
142+
lifecycle: SnsSwapLifecycle.Committed,
143+
swapOpenTimestampSeconds: BigInt(168_000_000),
144+
projectName: "Committed, Very Active Project",
145+
},
146+
icpCommitment: 10_000_000n,
147+
metrics: {
148+
...mockSnsMetrics,
149+
num_recently_executed_proposals: 22,
150+
},
108151
});
109152

110153
const po = renderComponent({
111-
snsProjects: [project1, project2],
154+
snsProjects: [project0, project1, project12, project3, project4],
112155
openSnsProposals: [],
113156
});
114157

@@ -117,13 +160,17 @@ describe("Launchpad2", () => {
117160
await launchedProjectsCards.getCardEntries();
118161

119162
expect(await launchedProjectsCards.isPresent()).toBe(true);
120-
expect(launchedProjectsCardsEntryPos.length).toBe(2);
121-
122-
expect(await launchedProjectsCardsEntryPos[0].getCardTitle()).toEqual(
123-
"Committed Project"
124-
);
125-
expect(await launchedProjectsCardsEntryPos[1].getCardTitle()).toEqual(
126-
"Not Committed Project"
163+
expect(launchedProjectsCardsEntryPos.length).toBe(5);
164+
const launchedProjectsCardsTitles = await Promise.all(
165+
launchedProjectsCardsEntryPos.map((card) => card.getCardTitle())
127166
);
167+
168+
expect(launchedProjectsCardsTitles).toEqual([
169+
"Committed, Very Active Project",
170+
"Committed, Not Active Project",
171+
"Not Committed, Very Active Project",
172+
"Not Committed, Not Active Project",
173+
"Outdated Project",
174+
]);
128175
});
129176
});

0 commit comments

Comments
 (0)