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

Add error state to icpSwapUsdPricesStore #5953

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
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
7 changes: 5 additions & 2 deletions frontend/src/lib/components/ui/UsdValueBanner.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { icpSwapUsdPricesStore } from "$lib/derived/icp-swap.derived";
import { i18n } from "$lib/stores/i18n";
import { formatNumber } from "$lib/utils/format.utils";
import { nonNullish } from "@dfinity/utils";
import { isNullish, nonNullish } from "@dfinity/utils";

export let usdAmount: number | undefined;

Expand All @@ -17,7 +17,10 @@
: absentValue;

let icpPrice: number | undefined;
$: icpPrice = $icpSwapUsdPricesStore?.[LEDGER_CANISTER_ID.toText()];
$: icpPrice =
isNullish($icpSwapUsdPricesStore) || $icpSwapUsdPricesStore === "error"
? undefined
: $icpSwapUsdPricesStore[LEDGER_CANISTER_ID.toText()];

let icpPriceFormatted: string;
$: icpPriceFormatted = nonNullish(icpPrice)
Expand Down
90 changes: 59 additions & 31 deletions frontend/src/lib/derived/icp-swap.derived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,69 @@ import { icpSwapTickersStore } from "$lib/stores/icp-swap.store";
import type { IcpSwapTicker } from "$lib/types/icp-swap";
import { mapEntries } from "$lib/utils/utils";
import { isNullish } from "@dfinity/utils";
import { derived } from "svelte/store";
import { derived, type Readable } from "svelte/store";

export type IcpSwapUsdPricesStoreData =
| Record<string, number>
| undefined
| "error";

export type IcpSwapUsdPricesStore = Readable<IcpSwapUsdPricesStoreData>;

/// Holds a record mapping ledger canister IDs to the ckUSDC price of their
/// tokens.
export const icpSwapUsdPricesStore = derived(icpSwapTickersStore, (tickers) => {
if (isNullish(tickers)) {
return undefined;
}
const icpLedgerCanisterId = LEDGER_CANISTER_ID.toText();
const ledgerCanisterIdToTicker: Record<string, IcpSwapTicker> =
Object.fromEntries(
tickers
// Only keep ICP based tickers
.filter((ticker) => ticker.target_id === icpLedgerCanisterId)
.map((ticker) => [ticker.base_id, ticker])
);

const ckusdcTicker =
ledgerCanisterIdToTicker[CKUSDC_LEDGER_CANISTER_ID.toText()];
if (isNullish(ckusdcTicker)) {
return {};
}
export const icpSwapUsdPricesStore: IcpSwapUsdPricesStore = derived(
icpSwapTickersStore,
(tickers) => {
if (isNullish(tickers)) {
return undefined;
}
// The contents of icpSwapTickersStore come from ICP Swap, so there's no
// guarantee that it's format is as expected.
try {
const icpLedgerCanisterId = LEDGER_CANISTER_ID.toText();
const ledgerCanisterIdToTicker: Record<string, IcpSwapTicker> =
Object.fromEntries(
tickers
// Only keep ICP based tickers
.filter((ticker) => ticker.target_id === icpLedgerCanisterId)
.map((ticker) => [ticker.base_id, ticker])
);

const icpPriceInCkusdc = Number(ckusdcTicker?.last_price);
const ckusdcTicker =
ledgerCanisterIdToTicker[CKUSDC_LEDGER_CANISTER_ID.toText()];
if (isNullish(ckusdcTicker)) {
return "error";
}

const ledgerCanisterIdToUsdPrice: Record<string, number> = mapEntries({
obj: ledgerCanisterIdToTicker,
mapFn: ([ledgerCanisterId, ticker]) => [
ledgerCanisterId,
icpPriceInCkusdc / Number(ticker.last_price),
],
});
const icpPriceInCkusdc = Number(ckusdcTicker?.last_price);

// There is no ticker for ICP to ICP but we do want the ICP price in ckUSDC.
ledgerCanisterIdToUsdPrice[LEDGER_CANISTER_ID.toText()] = icpPriceInCkusdc;
if (icpPriceInCkusdc === 0 || !Number.isFinite(icpPriceInCkusdc)) {
return "error";
}

return ledgerCanisterIdToUsdPrice;
});
const ledgerCanisterIdToUsdPrice: Record<string, number> = mapEntries({
obj: ledgerCanisterIdToTicker,
mapFn: ([ledgerCanisterId, ticker]) => {
const lastPrice = Number(ticker.last_price);
if (lastPrice === 0 || !Number.isFinite(lastPrice)) {
return undefined;
}
return [
ledgerCanisterId,
icpPriceInCkusdc / Number(ticker.last_price),
];
},
});

// There is no ticker for ICP to ICP but we do want the ICP price in ckUSDC.
ledgerCanisterIdToUsdPrice[LEDGER_CANISTER_ID.toText()] =
icpPriceInCkusdc;

return ledgerCanisterIdToUsdPrice;
} catch (error) {
console.error(error);
return "error";
}
}
);
17 changes: 9 additions & 8 deletions frontend/src/lib/derived/icp-tokens-list-user.derived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
icpAccountsStore,
type IcpAccountsStore,
} from "$lib/derived/icp-accounts.derived";
import { icpSwapUsdPricesStore } from "$lib/derived/icp-swap.derived";
import {
icpSwapUsdPricesStore,
type IcpSwapUsdPricesStore,
} from "$lib/derived/icp-swap.derived";
import { i18n } from "$lib/stores/i18n";
import type { Account, AccountType } from "$lib/types/account";
import { UserTokenAction, type UserToken } from "$lib/types/tokens-page";
Expand Down Expand Up @@ -99,17 +102,15 @@ const convertAccountToUserTokenData = ({
};

export const icpTokensListUser = derived<
[
Readable<Universe>,
IcpAccountsStore,
Readable<I18n>,
Readable<Record<string, number> | undefined>,
],
[Readable<Universe>, IcpAccountsStore, Readable<I18n>, IcpSwapUsdPricesStore],
UserToken[]
>(
[nnsUniverseStore, icpAccountsStore, i18n, icpSwapUsdPricesStore],
([nnsUniverse, icpAccounts, i18nObj, icpSwapUsdPrices]) => {
const icpPrice = icpSwapUsdPrices?.[LEDGER_CANISTER_ID.toText()];
const icpPrice =
isNullish(icpSwapUsdPrices) || icpSwapUsdPrices === "error"
? undefined
: icpSwapUsdPrices[LEDGER_CANISTER_ID.toText()];
return [
convertAccountToUserTokenData({
nnsUniverse,
Expand Down
16 changes: 10 additions & 6 deletions frontend/src/lib/derived/tokens-list-user.derived.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { OWN_CANISTER_ID_TEXT } from "$lib/constants/canister-ids.constants";
import { icpSwapUsdPricesStore } from "$lib/derived/icp-swap.derived";
import {
icpSwapUsdPricesStore,
type IcpSwapUsdPricesStore,
type IcpSwapUsdPricesStoreData,
} from "$lib/derived/icp-swap.derived";
import { failedExistentImportedTokenLedgerIdsStore } from "$lib/derived/imported-tokens.derived";
import type { IcrcTokenMetadata } from "$lib/types/icrc";
import {
Expand All @@ -11,7 +15,7 @@ import { sumAccounts } from "$lib/utils/accounts.utils";
import { buildAccountsUrl, buildWalletUrl } from "$lib/utils/navigation.utils";
import { isUniverseNns } from "$lib/utils/universe.utils";
import { toUserTokenFailed } from "$lib/utils/user-token.utils";
import { TokenAmountV2, isNullish } from "@dfinity/utils";
import { isNullish, TokenAmountV2 } from "@dfinity/utils";
import { derived, type Readable } from "svelte/store";
import type { UniversesAccounts } from "./accounts-list.derived";
import { tokensListBaseStore } from "./tokens-list-base.derived";
Expand All @@ -25,13 +29,13 @@ const getUsdValue = ({
}: {
balance: TokenAmountV2;
ledgerCanisterId: string;
icpSwapUsdPrices: Record<string, number> | undefined;
icpSwapUsdPrices: IcpSwapUsdPricesStoreData;
}): number | undefined => {
const balanceE8s = Number(balance.toE8s());
if (balanceE8s === 0) {
return 0;
}
if (isNullish(icpSwapUsdPrices)) {
if (isNullish(icpSwapUsdPrices) || icpSwapUsdPrices === "error") {
return undefined;
}
const tokenUsdPrice = icpSwapUsdPrices[ledgerCanisterId];
Expand All @@ -50,7 +54,7 @@ const convertToUserTokenData = ({
accounts: UniversesAccounts;
tokensByUniverse: Record<string, IcrcTokenMetadata>;
baseTokenData: UserTokenBase;
icpSwapUsdPrices: Record<string, number> | undefined;
icpSwapUsdPrices: IcpSwapUsdPricesStoreData;
}): UserToken => {
const token = tokensByUniverse[baseTokenData.universeId.toText()];
const rowHref = isUniverseNns(baseTokenData.universeId)
Expand Down Expand Up @@ -109,7 +113,7 @@ export const tokensListUserStore = derived<
Readable<UniversesAccounts>,
Readable<Record<string, IcrcTokenMetadata>>,
Readable<Array<string>>,
Readable<Record<string, number> | undefined>,
IcpSwapUsdPricesStore,
],
UserToken[]
>(
Expand Down
60 changes: 56 additions & 4 deletions frontend/src/tests/lib/derived/icp-swap.derived.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CKETH_LEDGER_CANISTER_ID } from "$lib/constants/cketh-canister-ids.cons
import { CKUSDC_LEDGER_CANISTER_ID } from "$lib/constants/ckusdc-canister-ids.constants";
import { icpSwapUsdPricesStore } from "$lib/derived/icp-swap.derived";
import { icpSwapTickersStore } from "$lib/stores/icp-swap.store";
import type { IcpSwapTicker } from "$lib/types/icp-swap";
import { mockIcpSwapTicker } from "$tests/mocks/icp-swap.mock";
import { get } from "svelte/store";

Expand All @@ -12,14 +13,42 @@ describe("icp-swap.derived", () => {
expect(get(icpSwapUsdPricesStore)).toBeUndefined();
});

it("should be empty if there are no tickers", () => {
it("should be 'error' if tickers are not an array", () => {
// This is theoretically possible because the tickers store content is
// read from ICP Swap, which we have no control over.
icpSwapTickersStore.set({} as unknown as IcpSwapTicker[]);

vi.spyOn(console, "error").mockReturnValue();

expect(get(icpSwapUsdPricesStore)).toEqual("error");

expect(console.error).toBeCalledWith(
new TypeError("tickers.filter is not a function")
);
expect(console.error).toBeCalledTimes(1);
});

it("should be 'error' if there are no tickers", () => {
icpSwapTickersStore.set([]);
expect(get(icpSwapUsdPricesStore)).toEqual({});
expect(get(icpSwapUsdPricesStore)).toEqual("error");
});

it("should be empty if there is no ckUSDC ticker", () => {
it("should be 'error' if there is no ckUSDC ticker", () => {
icpSwapTickersStore.set([mockIcpSwapTicker]);
expect(get(icpSwapUsdPricesStore)).toEqual({});
expect(get(icpSwapUsdPricesStore)).toEqual("error");
});

it("should be 'error' if ICP price in ckUSDC is zero", () => {
const icpPriceInUsd = 0.0;

const ckusdcTicker = {
...mockIcpSwapTicker,
base_id: CKUSDC_LEDGER_CANISTER_ID.toText(),
last_price: `${icpPriceInUsd}`,
};

icpSwapTickersStore.set([ckusdcTicker]);
expect(get(icpSwapUsdPricesStore)).toEqual("error");
});

it("should have an ICP price if there is a ckUSDC ticker", () => {
Expand Down Expand Up @@ -85,5 +114,28 @@ describe("icp-swap.derived", () => {
[CKUSDC_LEDGER_CANISTER_ID.toText()]: 1,
});
});

it("should not divide by zero for zero price", () => {
const icpPriceInUsd = 12.4;
const icpPriceInCketh = 0.0;

const ckusdcTicker = {
...mockIcpSwapTicker,
base_id: CKUSDC_LEDGER_CANISTER_ID.toText(),
last_price: `${icpPriceInUsd}`,
};
const ckethTicker = {
...mockIcpSwapTicker,
base_id: CKETH_LEDGER_CANISTER_ID.toText(),
last_price: `${icpPriceInCketh}`,
};

icpSwapTickersStore.set([ckusdcTicker, ckethTicker]);
expect(get(icpSwapUsdPricesStore)).toEqual({
[LEDGER_CANISTER_ID.toText()]: icpPriceInUsd,
[CKUSDC_LEDGER_CANISTER_ID.toText()]: 1,
// No entry for CKETH_LEDGER_CANISTER_ID
});
});
});
});
Loading