diff --git a/src/frontend/src/lib/components/exchange/ExchangeBalance.svelte b/src/frontend/src/lib/components/exchange/ExchangeBalance.svelte
index 0c633ac7f5..bce74c5315 100644
--- a/src/frontend/src/lib/components/exchange/ExchangeBalance.svelte
+++ b/src/frontend/src/lib/components/exchange/ExchangeBalance.svelte
@@ -1,6 +1,6 @@
diff --git a/src/frontend/src/lib/components/hero/HeroContent.svelte b/src/frontend/src/lib/components/hero/HeroContent.svelte
index 6538761ae2..c44bba688c 100644
--- a/src/frontend/src/lib/components/hero/HeroContent.svelte
+++ b/src/frontend/src/lib/components/hero/HeroContent.svelte
@@ -14,7 +14,11 @@
import TokenLogo from '$lib/components/tokens/TokenLogo.svelte';
import SkeletonLogo from '$lib/components/ui/SkeletonLogo.svelte';
import { SLIDE_PARAMS } from '$lib/constants/transition.constants';
- import { balance, balanceZero } from '$lib/derived/balances.derived';
+ import {
+ balance,
+ balanceZero,
+ noPositiveBalanceAndNotAllBalancesZero
+ } from '$lib/derived/balances.derived';
import { exchangeInitialized, exchanges } from '$lib/derived/exchange.derived';
import { networkBitcoin, networkEthereum, networkICP } from '$lib/derived/network.derived';
import { pageToken } from '$lib/derived/page-token.derived';
@@ -45,7 +49,9 @@
});
$: loading.set(
- isRouteTransactions($page) ? isNullish(pageTokenUi?.balance) : !$exchangeInitialized
+ isRouteTransactions($page)
+ ? isNullish(pageTokenUi?.balance)
+ : !$exchangeInitialized || $noPositiveBalanceAndNotAllBalancesZero
);
let isTransactionsPage = false;
diff --git a/src/frontend/src/lib/derived/balances.derived.ts b/src/frontend/src/lib/derived/balances.derived.ts
index a9f413fcf3..c4da929b6a 100644
--- a/src/frontend/src/lib/derived/balances.derived.ts
+++ b/src/frontend/src/lib/derived/balances.derived.ts
@@ -1,15 +1,18 @@
+import { enabledNetworkTokens } from '$lib/derived/network-tokens.derived';
import { balancesStore } from '$lib/stores/balances.store';
import { token } from '$lib/stores/token.store';
import type { OptionBalance } from '$lib/types/balance';
-import { checkAnyNonZeroBalance } from '$lib/utils/balances.utils';
+import { checkAllBalancesZero, checkAnyNonZeroBalance } from '$lib/utils/balances.utils';
import { nonNullish } from '@dfinity/utils';
import { derived, type Readable } from 'svelte/store';
+// TODO: Create tests for this store
export const balance: Readable = derived(
[balancesStore, token],
([$balanceStore, $token]) => (nonNullish($token) ? $balanceStore?.[$token.id]?.data : undefined)
);
+// TODO: Create tests for this store
export const balanceZero: Readable = derived(
[balancesStore, token],
([$balanceStore, $token]) =>
@@ -19,6 +22,23 @@ export const balanceZero: Readable = derived(
$balanceStore[$token.id]?.data.isZero() === true
);
+// TODO: Create tests for this store
export const anyBalanceNonZero: Readable = derived([balancesStore], ([$balanceStore]) =>
checkAnyNonZeroBalance($balanceStore)
);
+
+// TODO: Create tests for this store
+export const allBalancesZero: Readable = derived(
+ [balancesStore, enabledNetworkTokens],
+ ([$balancesStore, $enabledNetworkTokens]) =>
+ checkAllBalancesZero({
+ $balancesStore: $balancesStore,
+ minLength: $enabledNetworkTokens.length
+ })
+);
+
+// TODO: Create tests for this store
+export const noPositiveBalanceAndNotAllBalancesZero: Readable = derived(
+ [anyBalanceNonZero, allBalancesZero],
+ ([$anyBalanceNonZero, $allBalancesZero]) => !$anyBalanceNonZero && !$allBalancesZero
+);
diff --git a/src/frontend/src/lib/derived/network-tokens.derived.ts b/src/frontend/src/lib/derived/network-tokens.derived.ts
index 592fbe729a..ea0e63bcfb 100644
--- a/src/frontend/src/lib/derived/network-tokens.derived.ts
+++ b/src/frontend/src/lib/derived/network-tokens.derived.ts
@@ -10,6 +10,7 @@ import { derived, type Readable } from 'svelte/store';
/**
* All user-enabled tokens matching the selected network or chain fusion.
*/
+// TODO: Create tests for this store
export const enabledNetworkTokens: Readable = derived(
[enabledTokens, selectedNetwork, pseudoNetworkChainFusion],
filterTokensForSelectedNetwork
diff --git a/src/frontend/src/lib/utils/balances.utils.ts b/src/frontend/src/lib/utils/balances.utils.ts
index 9141462abe..7b7edbdabe 100644
--- a/src/frontend/src/lib/utils/balances.utils.ts
+++ b/src/frontend/src/lib/utils/balances.utils.ts
@@ -1,6 +1,7 @@
import { type BalancesData } from '$lib/stores/balances.store';
import type { CertifiedStoreData } from '$lib/stores/certified.store';
import type { TokenId } from '$lib/types/token';
+import type { Option } from '$lib/types/utils';
import { nonNullish } from '@dfinity/utils';
export const checkAnyNonZeroBalance = ($balancesStore: CertifiedStoreData): boolean =>
@@ -8,3 +9,30 @@ export const checkAnyNonZeroBalance = ($balancesStore: CertifiedStoreData !($balancesStore[tokenId as TokenId]?.data?.isZero() ?? true)
);
+
+/**
+ * Check if all balances are zero.
+ *
+ * It requires a minimum length of the balance data to be considered valid.
+ * This is to avoid false positives when, for example, the list of tokens is still loading,
+ * and the number of tokens in the balance store is not the same as the number of tokens in the UI.
+ *
+ * @param $balancesStore - Certified store of balances.
+ * @param minLength - Minimum length of the store to be considered valid.
+ * @returns `true` if all balances are zero and the conditions are met, `false` otherwise.
+ */
+
+export const checkAllBalancesZero = ({
+ $balancesStore,
+ minLength
+}: {
+ $balancesStore: CertifiedStoreData;
+ minLength: number;
+}): boolean =>
+ nonNullish($balancesStore) &&
+ Object.getOwnPropertySymbols($balancesStore).length >= Math.max(minLength, 1) &&
+ Object.getOwnPropertySymbols($balancesStore).every((tokenId) => {
+ const balance: Option = $balancesStore[tokenId as TokenId];
+
+ return balance === null || (balance?.data?.isZero() ?? false) || balance?.data === null;
+ });
diff --git a/src/frontend/src/tests/lib/utils/balances.utils.spec.ts b/src/frontend/src/tests/lib/utils/balances.utils.spec.ts
index 6fed121b2b..ef96246c87 100644
--- a/src/frontend/src/tests/lib/utils/balances.utils.spec.ts
+++ b/src/frontend/src/tests/lib/utils/balances.utils.spec.ts
@@ -1,7 +1,7 @@
import { ZERO } from '$lib/constants/app.constants';
import type { BalancesData } from '$lib/stores/balances.store';
import type { CertifiedStoreData } from '$lib/stores/certified.store';
-import { checkAnyNonZeroBalance } from '$lib/utils/balances.utils';
+import { checkAllBalancesZero, checkAnyNonZeroBalance } from '$lib/utils/balances.utils';
import { bn1 } from '$tests/mocks/balances.mock';
describe('checkAnyNonZeroBalance', () => {
@@ -58,3 +58,95 @@ describe('checkAnyNonZeroBalance', () => {
expect(checkAnyNonZeroBalance(undefined)).toBe(false);
});
});
+
+describe('checkAllBalancesZero', () => {
+ it('should return false if there is at least one non-zero balance', () => {
+ const mockBalancesStore: CertifiedStoreData = {
+ [Symbol('token1')]: { data: bn1 },
+ [Symbol('token2')]: { data: ZERO }
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 1 })).toBe(false);
+ });
+
+ it('should return false if there is at least one non-zero balance and one nullish balance', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: { data: bn1 },
+ [Symbol('token2')]: undefined
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 1 })).toBe(false);
+ });
+
+ it('should return false if there is at least one zero balance and one undefined balance', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: { data: ZERO },
+ [Symbol('token2')]: undefined
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 1 })).toBe(false);
+ });
+
+ it('should return true if there is at least one zero balance and one null balance', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: { data: ZERO },
+ [Symbol('token2')]: null
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 1 })).toBe(true);
+ });
+
+ it('should return true if all balances are zero', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: { data: ZERO },
+ [Symbol('token2')]: { data: ZERO }
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 1 })).toBe(true);
+ });
+
+ it('should return false if balances data are nullish', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: { data: null },
+ [Symbol('token2')]: { data: undefined }
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 1 })).toBe(false);
+ });
+
+ it('should return false if balances are nullish', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: null,
+ [Symbol('token2')]: undefined
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 1 })).toBe(false);
+ });
+
+ it('should return false if store is empty and minimum length is 0', () => {
+ expect(checkAllBalancesZero({ $balancesStore: {}, minLength: 0 })).toBe(false);
+ });
+
+ it('should return false if store is empty and minimum length is 1', () => {
+ expect(checkAllBalancesZero({ $balancesStore: {}, minLength: 1 })).toBe(false);
+ });
+
+ it('should return false if minimum length is not met', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: { data: ZERO },
+ [Symbol('token2')]: { data: ZERO }
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 3 })).toBe(false);
+ });
+
+ it('should return true if minimum length is met', () => {
+ const mockBalancesStore = {
+ [Symbol('token1')]: { data: ZERO },
+ [Symbol('token2')]: { data: ZERO },
+ [Symbol('token3')]: { data: ZERO }
+ } as unknown as CertifiedStoreData;
+
+ expect(checkAllBalancesZero({ $balancesStore: mockBalancesStore, minLength: 3 })).toBe(true);
+ });
+});
diff --git a/src/frontend/src/tests/utils/derived.utils.ts b/src/frontend/src/tests/utils/derived.utils.ts
index 237c6ef8ae..505a4b076b 100644
--- a/src/frontend/src/tests/utils/derived.utils.ts
+++ b/src/frontend/src/tests/utils/derived.utils.ts
@@ -8,7 +8,12 @@ import {
ethAddressNotLoaded
} from '$lib/derived/address.derived';
import { authIdentity, authNotSignedIn, authSignedIn } from '$lib/derived/auth.derived';
-import { balance, balanceZero } from '$lib/derived/balances.derived';
+import {
+ allBalancesZero,
+ anyBalanceNonZero,
+ balance,
+ balanceZero
+} from '$lib/derived/balances.derived';
import { isBusy } from '$lib/derived/busy.derived';
import { exchangeInitialized, exchanges } from '$lib/derived/exchange.derived';
import { userHasPouhCredential } from '$lib/derived/has-pouh-credential';
@@ -51,6 +56,8 @@ import type { Readable, Unsubscriber } from 'svelte/store';
import type { MockInstance } from 'vitest';
const derivedList: Record> = {
+ allBalancesZero,
+ anyBalanceNonZero,
authIdentity,
authNotSignedIn,
authSignedIn,