From 580ff35f96c84e0efae9e46c60535338de74f9da Mon Sep 17 00:00:00 2001 From: Yusef Habib Date: Wed, 8 Jan 2025 10:22:48 +0100 Subject: [PATCH] feat: add accounts-balance loading service for the Portfolio page (#6090) # Motivation The new Portfolio page shares some similarities with the existing `/token` page, as both aim to load the balancers for SNS, ICRP, and BTC. They load these accounts each time the user navigates to the respective pages, but they try to avoid loading them multiple times due to re-renders by tracking the requested accounts. This first PR extracts some of the logic for use in both pages. A follow-up PR will implement this logic for the Portfolio page. Later, it can also be applied to the Token page. [Ticket](https://dfinity.atlassian.net/browse/NNS1-3520) # Changes - `loadSnsAccountsBalances` loads the balances of SNS projects. - `loadAccountsBalances` loadsthe balances of ICRC and ckBTC. - A mechanism tracks loaded balances and cleans them when the consumer page is mounted. # Tests - Unit tests # Todos - [ ] Add entry to changelog (if necessary). Not necessary Next PR: #6116 --- .../services/accounts-balances.services.ts | 41 +++++++ .../accounts-balances.services.spec.ts | 109 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 frontend/src/lib/services/accounts-balances.services.ts create mode 100644 frontend/src/tests/lib/services/accounts-balances.services.spec.ts diff --git a/frontend/src/lib/services/accounts-balances.services.ts b/frontend/src/lib/services/accounts-balances.services.ts new file mode 100644 index 00000000000..35a4af61d68 --- /dev/null +++ b/frontend/src/lib/services/accounts-balances.services.ts @@ -0,0 +1,41 @@ +import { uncertifiedLoadSnsesAccountsBalances } from "$lib/services/sns-accounts-balance.services"; +import { uncertifiedLoadAccountsBalance } from "$lib/services/wallet-uncertified-accounts.services"; +import type { UniverseCanisterIdText } from "$lib/types/universe"; +import type { CanisterIdString } from "@dfinity/nns"; +import { Principal } from "@dfinity/principal"; + +const loadedBalances = new Set(); +export const resetBalanceLoading = (): void => { + loadedBalances.clear(); +}; + +const getNotLoadedIds = (ids: CanisterIdString[]): CanisterIdString[] => { + const notLoadedIds = ids.filter((id) => !loadedBalances.has(id)); + notLoadedIds.forEach((id) => loadedBalances.add(id)); + return notLoadedIds; +}; + +export const loadSnsAccountsBalances = async ( + rootCanisterIds: Principal[] +): Promise => { + const stringIds = rootCanisterIds.map((id) => id.toText()); + const notLoadedIds = getNotLoadedIds(stringIds); + + if (notLoadedIds.length === 0) return; + + await uncertifiedLoadSnsesAccountsBalances({ + rootCanisterIds: notLoadedIds.map((id) => Principal.fromText(id)), + }); +}; + +export const loadAccountsBalances = async ( + universeIds: UniverseCanisterIdText[] +): Promise => { + const notLoadedIds = getNotLoadedIds(universeIds); + + if (notLoadedIds.length === 0) return; + + await uncertifiedLoadAccountsBalance({ + universeIds: notLoadedIds, + }); +}; diff --git a/frontend/src/tests/lib/services/accounts-balances.services.spec.ts b/frontend/src/tests/lib/services/accounts-balances.services.spec.ts new file mode 100644 index 00000000000..fb279801375 --- /dev/null +++ b/frontend/src/tests/lib/services/accounts-balances.services.spec.ts @@ -0,0 +1,109 @@ +import { + loadAccountsBalances, + loadSnsAccountsBalances, + resetBalanceLoading, +} from "$lib/services/accounts-balances.services"; +import * as snsBalanceServices from "$lib/services/sns-accounts-balance.services"; +import * as walletServices from "$lib/services/wallet-uncertified-accounts.services"; +import type { CanisterIdString } from "@dfinity/nns"; +import { Principal } from "@dfinity/principal"; + +vi.mock("$lib/services/icrc-accounts.services", () => { + return { + loadAccounts: vi.fn(), + loadIcrcToken: vi.fn(), + }; +}); + +vi.mock("$lib/services/sns-accounts.services", () => { + return { + loadSnsAccounts: vi.fn(), + }; +}); + +describe("accounts-balances services", () => { + let accountsBalanceSpy; + let snsBalancesSpy; + + beforeEach(() => { + resetBalanceLoading(); + + accountsBalanceSpy = vi.spyOn( + walletServices, + "uncertifiedLoadAccountsBalance" + ); + + snsBalancesSpy = vi.spyOn( + snsBalanceServices, + "uncertifiedLoadSnsesAccountsBalances" + ); + }); + + describe("loadSnsBalances", () => { + it("should not call service if array is empty", async () => { + await loadSnsAccountsBalances([]); + expect(snsBalancesSpy).not.toHaveBeenCalled(); + }); + + it("should call service with correct parameters", async () => { + const principal1 = Principal.fromText("rrkah-fqaaa-aaaaa-aaaaq-cai"); + const principal2 = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"); + + await loadSnsAccountsBalances([principal1, principal2]); + + expect(snsBalancesSpy).toHaveBeenCalledWith({ + rootCanisterIds: [principal1, principal2], + }); + }); + + it("should not reload already loaded canister IDs", async () => { + const principal1 = Principal.fromText("rrkah-fqaaa-aaaaa-aaaaq-cai"); + + await loadSnsAccountsBalances([principal1]); + await loadSnsAccountsBalances([principal1]); + + expect(snsBalancesSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("loadAccountsBalances", () => { + it("should not call service if array is empty", async () => { + await loadAccountsBalances([]); + expect(accountsBalanceSpy).not.toHaveBeenCalled(); + }); + + it("should call service with correct parameters", async () => { + const universeIds: CanisterIdString[] = [ + "rrkah-fqaaa-aaaaa-aaaaq-cai", + "ryjl3-tyaaa-aaaaa-aaaba-cai", + ]; + + await loadAccountsBalances(universeIds); + + expect(accountsBalanceSpy).toHaveBeenCalledWith({ + universeIds, + }); + }); + + it("should not reload already loaded universe IDs", async () => { + const universeIds: CanisterIdString[] = ["rrkah-fqaaa-aaaaa-aaaaq-cai"]; + + await loadAccountsBalances(universeIds); + await loadAccountsBalances(universeIds); + + expect(accountsBalanceSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("resetBalanceLoading", () => { + it("should allow reloading previously loaded IDs after reset", async () => { + const universeIds: CanisterIdString[] = ["rrkah-fqaaa-aaaaa-aaaaq-cai"]; + + await loadAccountsBalances(universeIds); + resetBalanceLoading(); + await loadAccountsBalances(universeIds); + + expect(accountsBalanceSpy).toHaveBeenCalledTimes(2); + }); + }); +});