Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(portfolio): new utility to get top projects for the ProjectsCard #6159

Merged
merged 4 commits into from
Jan 15, 2025
Merged
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
43 changes: 42 additions & 1 deletion frontend/src/lib/utils/portfolio.utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import type { TableProject } from "$lib/types/staking";
import type { UserToken, UserTokenData } from "$lib/types/tokens-page";
import {
createDescendingComparator,
mergeComparators,
} from "$lib/utils/sort.utils";
import {
compareByProjectTitle,
compareIcpFirst,
} from "$lib/utils/staking.utils";
import {
compareTokensByImportance,
compareTokensIcpFirst,
} from "$lib/utils/tokens-table.utils";
import { isUserTokenData } from "$lib/utils/user-token.utils";

const MAX_NUMBER_OF_ITEMS = 4;

const compareTokensByUsdBalance = createDescendingComparator(
(token: UserTokenData) => token?.balanceInUsd ?? 0 > 0
);
@@ -29,7 +36,7 @@ const compareTokens = mergeComparators([
*/
export const getTopTokens = ({
userTokens,
maxResults = 4,
maxResults = MAX_NUMBER_OF_ITEMS,
isSignedIn = false,
}: {
userTokens: UserToken[];
@@ -45,3 +52,37 @@ export const getTopTokens = ({

return topTokens.filter((token) => token?.balanceInUsd ?? 0 > 0);
};

const compareProjectsByUsdBalance = createDescendingComparator(
(project: TableProject) => project?.stakeInUsd ?? 0 > 0
);

const compareProjects = mergeComparators([
compareIcpFirst,
compareProjectsByUsdBalance,
compareByProjectTitle,
]);

/**
* Filters and sorts projects based on specific criteria:
* - Always prioritizes ICP project first
* - Sorts remaining projects by USD stake value
* - Sorts remaining projects by title alphabetically
* - Limits the number of returned projects to MAX_NUMBER_OF_ITEMS
* - When isSignedIn true, filters out projects with zero stake as we show only projects with guaranteed stake
*/
export const getTopProjects = ({
projects,
isSignedIn = false,
}: {
projects: TableProject[];
isSignedIn?: boolean;
}): TableProject[] => {
const topProjects = [...projects]
.sort(compareProjects)
.slice(0, MAX_NUMBER_OF_ITEMS);

if (!isSignedIn) return topProjects;

return topProjects.filter((project) => project?.stakeInUsd ?? 0 > 0);
};
4 changes: 2 additions & 2 deletions frontend/src/lib/utils/staking.utils.ts
Original file line number Diff line number Diff line change
@@ -198,15 +198,15 @@ export const getTableProjects = ({
});
};

const compareIcpFirst = createDescendingComparator(
export const compareIcpFirst = createDescendingComparator(
(project: TableProject) => project.universeId === OWN_CANISTER_ID_TEXT
);

const comparePositiveNeuronsFirst = createDescendingComparator(
(project: TableProject) => (project.neuronCount ?? 0) > 0
);

const compareByProjectTitle = createAscendingComparator(
export const compareByProjectTitle = createAscendingComparator(
(project: TableProject) => project.title.toLowerCase()
);

178 changes: 172 additions & 6 deletions frontend/src/tests/lib/utils/portfolio.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { CKBTC_UNIVERSE_CANISTER_ID } from "$lib/constants/ckbtc-canister-ids.constants";
import { CKUSDC_UNIVERSE_CANISTER_ID } from "$lib/constants/ckusdc-canister-ids.constants";
import type { TableProject } from "$lib/types/staking";
import type { UserToken } from "$lib/types/tokens-page";
import { getTopTokens } from "$lib/utils/portfolio.utils";
import { getTopProjects, getTopTokens } from "$lib/utils/portfolio.utils";
import { principal } from "$tests/mocks/sns-projects.mock";
import { mockTableProject } from "$tests/mocks/staking.mock";
import {
createIcpUserToken,
createUserToken,
@@ -12,17 +14,13 @@ import {
describe("Portfolio utils", () => {
describe("getTopTokens", () => {
const mockNonUserToken = createUserTokenLoading();

const mockIcpToken = createIcpUserToken({});

const mockIcpToken = createIcpUserToken();
const mockCkBTCToken = createUserToken({
universeId: CKBTC_UNIVERSE_CANISTER_ID,
});

const mockCkUSDCToken = createUserToken({
universeId: CKUSDC_UNIVERSE_CANISTER_ID,
});

const mockOtherToken = createUserToken({
universeId: principal(1),
});
@@ -156,4 +154,172 @@ describe("Portfolio utils", () => {
});
});
});

describe("getTopProjects", () => {
const mockIcpProject: TableProject = {
...mockTableProject,
title: "Internet Computer",
stakeInUsd: 0,
};

const mockProject1: TableProject = {
...mockTableProject,
title: "A Project",
stakeInUsd: 0,
universeId: "1",
};

const mockProject2: TableProject = {
...mockTableProject,
title: "B Project",
stakeInUsd: 0,
universeId: "2",
};

const mockProject3: TableProject = {
...mockTableProject,
title: "C Project",
stakeInUsd: 0,
universeId: "3",
};

const mockProject4: TableProject = {
...mockTableProject,
title: "D Project",
stakeInUsd: 0,
universeId: "4",
};

it("should respect the result limit of MAX_NUMBER_OF_ITEMS(4)", () => {
const projects = [
mockIcpProject,
mockProject1,
mockProject2,
mockProject3,
mockProject4,
];

const result = getTopProjects({
projects,
});

expect(result).toHaveLength(4);
expect(result).toEqual([
mockIcpProject,
mockProject1,
mockProject2,
mockProject3,
]);
});

it("should order projects: ICP first, then by project title value", () => {
const projects = [
mockProject2,
mockProject3,
mockProject1,
mockIcpProject,
];

const result = getTopProjects({ projects });

expect(result).toEqual([
mockIcpProject,
mockProject1,
mockProject2,
mockProject3,
]);
});

describe("when signed in", () => {
const mockIcpProject: TableProject = {
...mockTableProject,
title: "Internet Computer",
stakeInUsd: 100,
};

const mockProject1: TableProject = {
...mockTableProject,
title: "Alpha Project",
stakeInUsd: 2000,
universeId: "1",
};

const mockProject2: TableProject = {
...mockTableProject,
title: "Beta Project",
stakeInUsd: 1000,
universeId: "2",
};

const mockProject3: TableProject = {
...mockTableProject,
title: "Gamma Project",
stakeInUsd: 10,
universeId: "3",
};

const mockZeroStakeProject: TableProject = {
...mockTableProject,
title: "Zero Stake Project",
stakeInUsd: 0,
universeId: "4",
};

it("should exclude projects with zero stake", () => {
const projects = [
mockZeroStakeProject,
mockProject1,
mockProject2,
mockProject3,
mockIcpProject,
];

const result = getTopProjects({
projects,
isSignedIn: true,
});

expect(result).toHaveLength(4);
expect(result).not.toContainEqual(mockZeroStakeProject);
});

it("should order projects: ICP first, then by descending USD stake", () => {
const projects = [
mockProject3,
mockProject2,
mockProject1,
mockZeroStakeProject,
mockIcpProject,
];

const result = getTopProjects({
projects,
isSignedIn: true,
});

expect(result).toEqual([
mockIcpProject, // ICP first, 100$
mockProject1, // 2000$
mockProject2, // 1000$
mockProject3, // 10$
]);
});

it("should return empty array when all projects have zero stake", () => {
const zeroIcpProject: TableProject = {
...mockTableProject,
stakeInUsd: 0,
};

const projects = [mockZeroStakeProject, zeroIcpProject];

const result = getTopProjects({
projects,
isSignedIn: true,
});

expect(result).toHaveLength(0);
});
});
});
});