Skip to content

Commit

Permalink
Add index canister service (#5398)
Browse files Browse the repository at this point in the history
# Motivation

It's possible to import a custom token w/o providing the index canister
ID. There should be an option to add the index canister later. Here we
introduce the function that adds the missing index canister and safes
it.

# Changes

- Add addIndexCanister service.

# Tests

- Added.

# Todos

- [ ] Add entry to changelog (if necessary).
Not necessary.
  • Loading branch information
mstrasinskis committed Sep 3, 2024
1 parent 4f482c3 commit 42f0063
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 13 deletions.
2 changes: 2 additions & 0 deletions frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,7 @@
"load_imported_tokens": "There was an unexpected issue while loading imported tokens.",
"add_imported_token": "There was an unexpected issue while adding new imported token.",
"remove_imported_token": "There was an unexpected issue while removing the imported token.",
"update_imported_token": "There was an unexpected issue while updating the imported token.",
"too_many": "You can't import more than $limit tokens.",
"ledger_canister_loading": "Unable to load token details using the provided Ledger Canister ID.",
"is_duplication": "You have already imported this token, you can find it in the token list.",
Expand Down Expand Up @@ -1034,6 +1035,7 @@
"show_all": "Show all",
"add_imported_token_success": "New token has been successfully imported!",
"remove_imported_token_success": "The token has been successfully removed!",
"update_imported_token_success": "The token has been successfully updated!",
"ledger_canister": "Ledger Canister",
"index_canister": "Index Canister"
},
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/lib/services/imported-tokens.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
fromImportedTokenData,
toImportedTokenData,
} from "$lib/utils/imported-tokens.utils";
import type { Principal } from "@dfinity/principal";
import { isNullish } from "@dfinity/utils";
import { queryAndUpdate } from "./utils.services";

Expand Down Expand Up @@ -126,6 +127,46 @@ export const addImportedToken = async ({
return { success: false };
};

/**
* Add index canister ID to imported token.
* Note: This service function assumes the indexCanisterId is valid and matches the ledgerCanisterId.
* - Displays a success toast if the operation is successful.
* - Displays an error toast if the operation fails.
*/
export const addIndexCanister = async ({
ledgerCanisterId,
indexCanisterId,
importedTokens,
}: {
ledgerCanisterId: Principal;
indexCanisterId: Principal;
importedTokens: ImportedTokenData[];
}): Promise<{ success: boolean }> => {
const tokens = importedTokens.map((token) =>
token.ledgerCanisterId.toText() === ledgerCanisterId.toText()
? { ...token, indexCanisterId }
: token
);

const { err } = await saveImportedToken({ tokens });

if (isNullish(err)) {
await loadImportedTokens();
toastsSuccess({
labelKey: "tokens.update_imported_token_success",
});

return { success: true };
}

toastsError({
labelKey: "error__imported_tokens.update_imported_token",
err,
});

return { success: false };
};

/**
* Remove imported tokens and reload imported tokens from the `nns-dapp` backend to update the `importedTokensStore`.
* - Displays a success toast if the operation is successful.
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ interface I18nError__imported_tokens {
load_imported_tokens: string;
add_imported_token: string;
remove_imported_token: string;
update_imported_token: string;
too_many: string;
ledger_canister_loading: string;
is_duplication: string;
Expand Down Expand Up @@ -1092,6 +1093,7 @@ interface I18nTokens {
show_all: string;
add_imported_token_success: string;
remove_imported_token_success: string;
update_imported_token_success: string;
ledger_canister: string;
index_canister: string;
}
Expand Down
134 changes: 121 additions & 13 deletions frontend/src/tests/lib/services/imported-tokens.services.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import type { ImportedToken } from "$lib/canisters/nns-dapp/nns-dapp.types";
import {
addImportedToken,
addIndexCanister,
loadImportedTokens,
removeImportedTokens,
} from "$lib/services/imported-tokens.services";
Expand All @@ -14,6 +15,8 @@ import * as toastsStore from "$lib/stores/toasts.store";
import type { ImportedTokenData } from "$lib/types/imported-tokens";
import { mockIdentity, resetIdentity } from "$tests/mocks/auth.store.mock";
import { principal } from "$tests/mocks/sns-projects.mock";
import { toastsStore as toastsStoreEntry } from "@dfinity/gix-components";
import * as dfinityUtils from "@dfinity/utils";
import { get } from "svelte/store";

describe("imported-tokens-services", () => {
Expand All @@ -39,7 +42,9 @@ describe("imported-tokens-services", () => {
vi.clearAllMocks();
resetIdentity();
importedTokensStore.reset();
toastsStoreEntry.reset();
vi.spyOn(console, "error").mockReturnValue();
vi.spyOn(dfinityUtils, "createAgent").mockReturnValue(undefined);
});

describe("loadImportedTokens", () => {
Expand All @@ -60,11 +65,11 @@ describe("imported-tokens-services", () => {
await loadImportedTokens();

expect(spyGetImportedTokens).toBeCalledTimes(2);
expect(spyGetImportedTokens).toHaveBeenCalledWith({
expect(spyGetImportedTokens).toBeCalledWith({
certified: false,
identity: mockIdentity,
});
expect(spyGetImportedTokens).toHaveBeenCalledWith({
expect(spyGetImportedTokens).toBeCalledWith({
certified: true,
identity: mockIdentity,
});
Expand Down Expand Up @@ -162,7 +167,7 @@ describe("imported-tokens-services", () => {

expect(success).toEqual(true);
expect(spySetImportedTokens).toBeCalledTimes(1);
expect(spySetImportedTokens).toHaveBeenCalledWith({
expect(spySetImportedTokens).toBeCalledWith({
identity: mockIdentity,
importedTokens: [importedTokenA, importedTokenB],
});
Expand Down Expand Up @@ -200,22 +205,22 @@ describe("imported-tokens-services", () => {
});

it("should display success toast", async () => {
const spyToastSuccsess = vi.spyOn(toastsStore, "toastsSuccess");
const spyToastSuccess = vi.spyOn(toastsStore, "toastsSuccess");
vi.spyOn(importedTokensApi, "setImportedTokens").mockRejectedValue(
undefined
);
vi.spyOn(importedTokensApi, "getImportedTokens").mockResolvedValue({
imported_tokens: [importedTokenA, importedTokenB],
});
expect(spyToastSuccsess).not.toBeCalled();
expect(spyToastSuccess).not.toBeCalled();

await addImportedToken({
tokenToAdd: importedTokenDataB,
importedTokens: [importedTokenDataA],
});

expect(spyToastSuccsess).toBeCalledTimes(1);
expect(spyToastSuccsess).toBeCalledWith({
expect(spyToastSuccess).toBeCalledTimes(1);
expect(spyToastSuccess).toBeCalledWith({
labelKey: "tokens.add_imported_token_success",
});
});
Expand Down Expand Up @@ -275,7 +280,7 @@ describe("imported-tokens-services", () => {

expect(success).toEqual(true);
expect(spySetImportedTokens).toBeCalledTimes(1);
expect(spySetImportedTokens).toHaveBeenCalledWith({
expect(spySetImportedTokens).toBeCalledWith({
identity: mockIdentity,
importedTokens: [importedTokenB],
});
Expand All @@ -294,7 +299,7 @@ describe("imported-tokens-services", () => {

expect(success).toEqual(true);
expect(spySetImportedTokens).toBeCalledTimes(1);
expect(spySetImportedTokens).toHaveBeenCalledWith({
expect(spySetImportedTokens).toBeCalledWith({
identity: mockIdentity,
importedTokens: [],
});
Expand Down Expand Up @@ -332,22 +337,22 @@ describe("imported-tokens-services", () => {
});

it("should display success toast", async () => {
const spyToastSuccsess = vi.spyOn(toastsStore, "toastsSuccess");
const spyToastSuccess = vi.spyOn(toastsStore, "toastsSuccess");
vi.spyOn(importedTokensApi, "setImportedTokens").mockRejectedValue(
undefined
);
vi.spyOn(importedTokensApi, "getImportedTokens").mockResolvedValue({
imported_tokens: [importedTokenB],
});
expect(spyToastSuccsess).not.toBeCalled();
expect(spyToastSuccess).not.toBeCalled();

await removeImportedTokens({
tokensToRemove: [importedTokenDataA],
importedTokens: [importedTokenDataA, importedTokenDataB],
});

expect(spyToastSuccsess).toBeCalledTimes(1);
expect(spyToastSuccsess).toBeCalledWith({
expect(spyToastSuccess).toBeCalledTimes(1);
expect(spyToastSuccess).toBeCalledWith({
labelKey: "tokens.remove_imported_token_success",
});
});
Expand All @@ -372,4 +377,107 @@ describe("imported-tokens-services", () => {
});
});
});

describe("addIndexCanister", () => {
const indexCanisterId = principal(1);
let spyGetImportedTokens;

beforeEach(() => {
spyGetImportedTokens = vi
.spyOn(importedTokensApi, "getImportedTokens")
.mockResolvedValue({
imported_tokens: [
importedTokenA,
{
...importedTokenB,
index_canister_id: [indexCanisterId],
},
],
});
});

it("should call setImportedTokens with updated token list", async () => {
const expectedTokenB = {
...importedTokenB,
index_canister_id: [indexCanisterId],
};
const expectedTokenDataB = {
...importedTokenDataB,
indexCanisterId,
};
const spySetImportedTokens = vi
.spyOn(importedTokensApi, "setImportedTokens")
.mockResolvedValue(undefined);

expect(importedTokenDataB.indexCanisterId).toBeUndefined();
importedTokensStore.set({
importedTokens: [importedTokenDataA, importedTokenDataB],
certified: true,
});
expect(spySetImportedTokens).toBeCalledTimes(0);
expect(spyGetImportedTokens).toBeCalledTimes(0);

const { success } = await addIndexCanister({
ledgerCanisterId: importedTokenDataB.ledgerCanisterId,
indexCanisterId,
importedTokens: [importedTokenDataA, importedTokenDataB],
});
expect(success).toEqual(true);
expect(spySetImportedTokens).toBeCalledTimes(1);
expect(spySetImportedTokens).toBeCalledWith({
identity: mockIdentity,
importedTokens: [importedTokenA, expectedTokenB],
});
expect(spySetImportedTokens).toBeCalledTimes(1);
// should reload imported tokens to update the store
expect(spyGetImportedTokens).toBeCalledTimes(2);
expect(get(importedTokensStore)).toEqual({
importedTokens: [importedTokenDataA, expectedTokenDataB],
certified: true,
});
});

it("should display success toast", async () => {
vi.spyOn(importedTokensApi, "setImportedTokens").mockResolvedValue(
undefined
);

expect(get(toastsStoreEntry)).toMatchObject([]);

await addIndexCanister({
ledgerCanisterId: importedTokenDataB.ledgerCanisterId,
indexCanisterId,
importedTokens: [importedTokenDataA, importedTokenDataB],
});

expect(get(toastsStoreEntry)).toMatchObject([
{
level: "success",
text: "The token has been successfully updated!",
},
]);
});

it("should display toast on error", async () => {
vi.spyOn(importedTokensApi, "setImportedTokens").mockRejectedValue(
new Error("test")
);

expect(get(toastsStoreEntry)).toMatchObject([]);

const { success } = await addIndexCanister({
ledgerCanisterId: importedTokenDataB.ledgerCanisterId,
indexCanisterId,
importedTokens: [importedTokenDataA, importedTokenDataB],
});

expect(success).toEqual(false);
expect(get(toastsStoreEntry)).toMatchObject([
{
level: "error",
text: "There was an unexpected issue while updating the imported token. test",
},
]);
});
});
});

0 comments on commit 42f0063

Please sign in to comment.