Skip to content

Commit

Permalink
feat: add accounts-balance loading service for the Portfolio page (#6090
Browse files Browse the repository at this point in the history
)

# 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
  • Loading branch information
yhabib authored Jan 8, 2025
1 parent 9239fa1 commit 580ff35
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 0 deletions.
41 changes: 41 additions & 0 deletions frontend/src/lib/services/accounts-balances.services.ts
Original file line number Diff line number Diff line change
@@ -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<CanisterIdString>();
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<void> => {
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<void> => {
const notLoadedIds = getNotLoadedIds(universeIds);

if (notLoadedIds.length === 0) return;

await uncertifiedLoadAccountsBalance({
universeIds: notLoadedIds,
});
};
109 changes: 109 additions & 0 deletions frontend/src/tests/lib/services/accounts-balances.services.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit 580ff35

Please sign in to comment.