diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 77bf1ebea7..347c977865 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -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.", @@ -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" }, diff --git a/frontend/src/lib/services/imported-tokens.services.ts b/frontend/src/lib/services/imported-tokens.services.ts index f77f6c568c..704f8d0cef 100644 --- a/frontend/src/lib/services/imported-tokens.services.ts +++ b/frontend/src/lib/services/imported-tokens.services.ts @@ -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"; @@ -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. diff --git a/frontend/src/lib/types/i18n.d.ts b/frontend/src/lib/types/i18n.d.ts index a5800d7ed0..4b966aa2f7 100644 --- a/frontend/src/lib/types/i18n.d.ts +++ b/frontend/src/lib/types/i18n.d.ts @@ -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; @@ -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; } diff --git a/frontend/src/tests/lib/services/imported-tokens.services.spec.ts b/frontend/src/tests/lib/services/imported-tokens.services.spec.ts index a91b35a19f..6618e416af 100644 --- a/frontend/src/tests/lib/services/imported-tokens.services.spec.ts +++ b/frontend/src/tests/lib/services/imported-tokens.services.spec.ts @@ -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"; @@ -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", () => { @@ -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", () => { @@ -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, }); @@ -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], }); @@ -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", }); }); @@ -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], }); @@ -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: [], }); @@ -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", }); }); @@ -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", + }, + ]); + }); + }); });