diff --git a/frontend/src/lib/types/staking.ts b/frontend/src/lib/types/staking.ts index 5a73da79868..e763edfb241 100644 --- a/frontend/src/lib/types/staking.ts +++ b/frontend/src/lib/types/staking.ts @@ -1,4 +1,5 @@ import type { ResponsiveTableColumn } from "$lib/types/responsive-table"; +import type { TokenAmountV2 } from "@dfinity/utils"; export type TableProject = { rowHref?: string; @@ -6,6 +7,7 @@ export type TableProject = { title: string; logo: string; neuronCount: number | undefined; + stake: TokenAmountV2 | undefined; }; export type ProjectsTableColumn = ResponsiveTableColumn; diff --git a/frontend/src/lib/utils/staking.utils.ts b/frontend/src/lib/utils/staking.utils.ts index 639f3d3791a..9189a51a200 100644 --- a/frontend/src/lib/utils/staking.utils.ts +++ b/frontend/src/lib/utils/staking.utils.ts @@ -2,10 +2,88 @@ import { OWN_CANISTER_ID_TEXT } from "$lib/constants/canister-ids.constants"; import type { TableProject } from "$lib/types/staking"; import type { Universe } from "$lib/types/universe"; import { buildNeuronsUrl } from "$lib/utils/navigation.utils"; -import { hasValidStake as nnsHasValidStake } from "$lib/utils/neuron.utils"; -import { hasValidStake as snsHasValidStake } from "$lib/utils/sns-neuron.utils"; +import { + neuronStake, + hasValidStake as nnsHasValidStake, +} from "$lib/utils/neuron.utils"; +import { + getSnsNeuronStake, + hasValidStake as snsHasValidStake, +} from "$lib/utils/sns-neuron.utils"; import type { NeuronInfo } from "@dfinity/nns"; import type { SnsNeuron } from "@dfinity/sns"; +import { ICPToken, TokenAmountV2, isNullish, type Token } from "@dfinity/utils"; + +const getNnsNeuronCountAndStake = ( + nnsNeurons: NeuronInfo[] | undefined +): { + neuronCount: number | undefined; + stake: bigint | undefined; + token: Token; +} => { + const neurons = nnsNeurons?.filter(nnsHasValidStake); + const stake = neurons?.reduce((acc, neuron) => acc + neuronStake(neuron), 0n); + return { + neuronCount: neurons?.length, + stake, + token: ICPToken, + }; +}; + +const getSnsNeuronCountAndStake = ({ + universe, + snsNeurons, +}: { + universe: Universe; + snsNeurons: { [rootCanisterId: string]: { neurons: SnsNeuron[] } }; +}): { + neuronCount: number | undefined; + stake: bigint | undefined; + token: Token | undefined; +} => { + const neurons = + snsNeurons[universe.canisterId]?.neurons.filter(snsHasValidStake); + const stake = neurons?.reduce( + (acc, neuron) => acc + getSnsNeuronStake(neuron), + 0n + ); + const token = universe.summary?.token; + return { + neuronCount: neurons?.length, + stake, + token, + }; +}; + +const getNeuronCountAndStake = ({ + isSignedIn, + universe, + nnsNeurons, + snsNeurons, +}: { + isSignedIn: boolean; + universe: Universe; + nnsNeurons: NeuronInfo[] | undefined; + snsNeurons: { [rootCanisterId: string]: { neurons: SnsNeuron[] } }; +}): { neuronCount: number | undefined; stake: TokenAmountV2 | undefined } => { + if (!isSignedIn) { + return { neuronCount: undefined, stake: undefined }; + } + const { neuronCount, stake, token } = + universe.canisterId === OWN_CANISTER_ID_TEXT + ? getNnsNeuronCountAndStake(nnsNeurons) + : getSnsNeuronCountAndStake({ snsNeurons, universe }); + return { + neuronCount, + stake: + isNullish(stake) || isNullish(token) + ? undefined + : TokenAmountV2.fromUlps({ + amount: stake, + token, + }), + }; +}; export const getTableProjects = ({ universes, @@ -19,12 +97,12 @@ export const getTableProjects = ({ snsNeurons: { [rootCanisterId: string]: { neurons: SnsNeuron[] } }; }): TableProject[] => { return universes.map((universe) => { - const neuronCount = !isSignedIn - ? undefined - : universe.canisterId === OWN_CANISTER_ID_TEXT - ? nnsNeurons?.filter(nnsHasValidStake).length - : snsNeurons[universe.canisterId]?.neurons.filter(snsHasValidStake) - .length; + const { neuronCount, stake } = getNeuronCountAndStake({ + isSignedIn, + universe, + nnsNeurons, + snsNeurons, + }); const rowHref = (neuronCount ?? 0) > 0 ? buildNeuronsUrl({ universe: universe.canisterId }) @@ -35,6 +113,7 @@ export const getTableProjects = ({ title: universe.title, logo: universe.logo, neuronCount, + stake, }; }); }; diff --git a/frontend/src/tests/lib/utils/staking.utils.spec.ts b/frontend/src/tests/lib/utils/staking.utils.spec.ts index 89f0b9c96d4..f857635ff43 100644 --- a/frontend/src/tests/lib/utils/staking.utils.spec.ts +++ b/frontend/src/tests/lib/utils/staking.utils.spec.ts @@ -4,22 +4,30 @@ import type { Universe } from "$lib/types/universe"; import { getTableProjects } from "$lib/utils/staking.utils"; import { mockNeuron } from "$tests/mocks/neurons.mock"; import { createMockSnsNeuron } from "$tests/mocks/sns-neurons.mock"; -import { principal } from "$tests/mocks/sns-projects.mock"; +import { + mockSnsToken, + mockSummary, + principal, +} from "$tests/mocks/sns-projects.mock"; +import { ICPToken, TokenAmountV2 } from "@dfinity/utils"; describe("staking.utils", () => { describe("getTableProjects", () => { const universeId2 = principal(2).toText(); - const nnsUniverse = { + const nnsUniverse: Universe = { canisterId: OWN_CANISTER_ID_TEXT, title: "Internet Computer", logo: IC_LOGO_ROUNDED, }; - const snsUniverse = { + const snsUniverse: Universe = { canisterId: universeId2, title: "title2", logo: "logo2", + summary: mockSummary.override({ + token: mockSnsToken, + }), }; const nnsHref = `/neurons/?u=${OWN_CANISTER_ID_TEXT}`; @@ -31,6 +39,10 @@ describe("staking.utils", () => { title: "Internet Computer", logo: IC_LOGO_ROUNDED, neuronCount: 0, + stake: TokenAmountV2.fromUlps({ + amount: 0n, + token: ICPToken, + }), }; const defaultExpectedSnsTableProject = { @@ -39,6 +51,10 @@ describe("staking.utils", () => { title: "title2", logo: "logo2", neuronCount: 0, + stake: TokenAmountV2.fromUlps({ + amount: 0n, + token: mockSnsToken, + }), }; const nnsNeuronWithStake = { @@ -87,11 +103,15 @@ describe("staking.utils", () => { ]); }); - it("should include number of NNS neurons", () => { + it("should include info for NNS neurons", () => { const tableProjects = getTableProjects({ universes: [nnsUniverse], isSignedIn: true, - nnsNeurons: [mockNeuron, mockNeuron, mockNeuron], + nnsNeurons: [ + nnsNeuronWithStake, + nnsNeuronWithStake, + nnsNeuronWithStake, + ], snsNeurons: {}, }); @@ -100,11 +120,15 @@ describe("staking.utils", () => { ...defaultExpectedNnsTableProject, rowHref: nnsHref, neuronCount: 3, + stake: TokenAmountV2.fromUlps({ + amount: 3n * nnsNeuronWithStake.fullNeuron.cachedNeuronStake, + token: ICPToken, + }), }, ]); }); - it("should include number of SNS neurons", () => { + it("should include info for SNS neurons", () => { const tableProjects = getTableProjects({ universes: [snsUniverse], isSignedIn: true, @@ -121,6 +145,10 @@ describe("staking.utils", () => { ...defaultExpectedSnsTableProject, rowHref: snsHref, neuronCount: 2, + stake: TokenAmountV2.fromUlps({ + amount: 2n * snsNeuronWithStake.cached_neuron_stake_e8s, + token: mockSnsToken, + }), }, ]); }); @@ -142,6 +170,10 @@ describe("staking.utils", () => { ...defaultExpectedNnsTableProject, rowHref: nnsHref, neuronCount: 1, + stake: TokenAmountV2.fromUlps({ + amount: nnsNeuronWithStake.fullNeuron.cachedNeuronStake, + token: ICPToken, + }), }, ]); }); @@ -168,6 +200,10 @@ describe("staking.utils", () => { ...defaultExpectedSnsTableProject, rowHref: snsHref, neuronCount: 1, + stake: TokenAmountV2.fromUlps({ + amount: snsNeuronWithStake.cached_neuron_stake_e8s, + token: mockSnsToken, + }), }, ]); }); @@ -189,11 +225,13 @@ describe("staking.utils", () => { ...defaultExpectedNnsTableProject, rowHref: undefined, neuronCount: undefined, + stake: undefined, }, { ...defaultExpectedSnsTableProject, rowHref: undefined, neuronCount: undefined, + stake: undefined, }, ]); }); @@ -213,11 +251,13 @@ describe("staking.utils", () => { ...defaultExpectedNnsTableProject, rowHref: undefined, neuronCount: undefined, + stake: undefined, }, { ...defaultExpectedSnsTableProject, rowHref: undefined, neuronCount: undefined, + stake: undefined, }, ]); }); diff --git a/frontend/src/tests/mocks/staking.mock.ts b/frontend/src/tests/mocks/staking.mock.ts index 00b42ae80e8..ba3d101d39b 100644 --- a/frontend/src/tests/mocks/staking.mock.ts +++ b/frontend/src/tests/mocks/staking.mock.ts @@ -1,6 +1,7 @@ import IC_LOGO_ROUNDED from "$lib/assets/icp-rounded.svg"; import { OWN_CANISTER_ID_TEXT } from "$lib/constants/canister-ids.constants"; import type { TableProject } from "$lib/types/staking"; +import { ICPToken, TokenAmountV2 } from "@dfinity/utils"; export const mockTableProject: TableProject = { rowHref: `/neurons/?u=${OWN_CANISTER_ID_TEXT}`, @@ -8,4 +9,8 @@ export const mockTableProject: TableProject = { title: "Internet Computer", logo: IC_LOGO_ROUNDED, neuronCount: 0, + stake: TokenAmountV2.fromUlps({ + amount: 100_000_000n, + token: ICPToken, + }), };