From 144f503574c55086d63c2ec0d1fc9516b68a6773 Mon Sep 17 00:00:00 2001 From: David de Kloet Date: Wed, 17 Jul 2024 17:52:18 +0200 Subject: [PATCH 1/9] Add canister method to store imported tokens --- rs/backend/nns-dapp-exports-production.txt | 2 + rs/backend/nns-dapp-exports-test.txt | 2 + rs/backend/nns-dapp.did | 24 ++++++++ rs/backend/src/accounts_store.rs | 50 +++++++++++++++++ rs/backend/src/accounts_store/schema/tests.rs | 1 + rs/backend/src/accounts_store/tests.rs | 56 +++++++++++++++++++ rs/backend/src/accounts_store/toy_data.rs | 1 + rs/backend/src/main.rs | 25 ++++++++- 8 files changed, 159 insertions(+), 2 deletions(-) diff --git a/rs/backend/nns-dapp-exports-production.txt b/rs/backend/nns-dapp-exports-production.txt index 9865d685dd2..a4610e6c178 100644 --- a/rs/backend/nns-dapp-exports-production.txt +++ b/rs/backend/nns-dapp-exports-production.txt @@ -7,6 +7,7 @@ canister_query get_canisters canister_query get_exceptional_transactions canister_query get_histogram canister_query get_stats +canister_query get_imported_tokens canister_query http_request canister_update add_account canister_update add_assets_tar_xz @@ -18,5 +19,6 @@ canister_update get_proposal_payload canister_update register_hardware_wallet canister_update rename_canister canister_update rename_sub_account +canister_update set_imported_tokens canister_update step_migration main diff --git a/rs/backend/nns-dapp-exports-test.txt b/rs/backend/nns-dapp-exports-test.txt index ac39452f086..338f64f1843 100644 --- a/rs/backend/nns-dapp-exports-test.txt +++ b/rs/backend/nns-dapp-exports-test.txt @@ -8,6 +8,7 @@ canister_query get_exceptional_transactions canister_query get_histogram canister_query get_stats canister_query get_toy_account +canister_query get_imported_tokens canister_query http_request canister_update add_account canister_update add_assets_tar_xz @@ -20,5 +21,6 @@ canister_update get_proposal_payload canister_update register_hardware_wallet canister_update rename_canister canister_update rename_sub_account +canister_update set_imported_tokens canister_update step_migration main diff --git a/rs/backend/nns-dapp.did b/rs/backend/nns-dapp.did index 8ba5ea617f3..55bf300ff07 100644 --- a/rs/backend/nns-dapp.did +++ b/rs/backend/nns-dapp.did @@ -193,6 +193,27 @@ type SchemaLabel = variant { AccountsInStableMemory; }; +type ImportedToken = record { + ledger_canister_id: principal; + index_canister_id: opt principal; +}; + +type ImportedTokens = record { + imported_tokens: vec ImportedToken; +}; + +type SetImportedTokensResponse = + variant { + Ok; + AccountNotFound; + }; + +type GetImportedTokensResponse = + variant { + Ok: ImportedTokens; + AccountNotFound; + }; + service: (opt Config) -> { get_account: () -> (GetAccountResponse) query; add_account: () -> (AccountIdentifier); @@ -216,4 +237,7 @@ service: (opt Config) -> { // Methods available in the test build only: get_toy_account: (nat64) -> (GetAccountResponse) query; + + set_imported_tokens: (ImportedTokens) -> (SetImportedTokensResponse); + get_imported_tokens: () -> (GetImportedTokensResponse) query; } diff --git a/rs/backend/src/accounts_store.rs b/rs/backend/src/accounts_store.rs index 5a2877948b3..598770ea4c9 100644 --- a/rs/backend/src/accounts_store.rs +++ b/rs/backend/src/accounts_store.rs @@ -154,6 +154,7 @@ pub struct Account { sub_accounts: HashMap, hardware_wallet_accounts: Vec, canisters: Vec, + imported_tokens: Option, // default_account_transactions: Do not reuse this field. There are still accounts in stable memor with this unused field. } @@ -216,6 +217,29 @@ impl PartialOrd for NamedCanister { } } +#[derive(CandidType, Clone, Copy, Default, Deserialize, Debug, Eq, PartialEq)] +pub struct ImportedToken { + ledger_canister_id: PrincipalId, + index_canister_id: Option, +} + +#[derive(CandidType, Clone, Default, Deserialize, Debug, Eq, PartialEq)] +pub struct ImportedTokens { + imported_tokens: Vec, +} + +#[derive(CandidType, Debug, PartialEq)] +pub enum SetImportedTokensResponse { + Ok, + AccountNotFound, +} + +#[derive(CandidType, Debug, PartialEq)] +pub enum GetImportedTokensResponse { + Ok(ImportedTokens), + AccountNotFound, +} + #[derive(Copy, Clone, CandidType, Deserialize, Debug, Eq, PartialEq)] pub enum TransactionType { Burn, @@ -746,6 +770,31 @@ impl AccountsStore { } } + pub fn set_imported_tokens( + &mut self, + caller: PrincipalId, + new_imported_tokens: ImportedTokens, + ) -> SetImportedTokensResponse { + let account_identifier = AccountIdentifier::from(caller).to_vec(); + let Some(mut account) = self.accounts_db.db_get_account(&account_identifier) else { + return SetImportedTokensResponse::AccountNotFound; + }; + + account.imported_tokens = Some(new_imported_tokens); + + self.accounts_db.db_insert_account(&account_identifier, account); + SetImportedTokensResponse::Ok + } + + pub fn get_imported_tokens(&mut self, caller: PrincipalId) -> GetImportedTokensResponse { + let account_identifier = AccountIdentifier::from(caller).to_vec(); + let Some(account) = self.accounts_db.db_get_account(&account_identifier) else { + return GetImportedTokensResponse::AccountNotFound; + }; + + GetImportedTokensResponse::Ok(account.imported_tokens.unwrap_or_default()) + } + #[must_use] pub fn get_block_height_synced_up_to(&self) -> Option { self.block_height_synced_up_to @@ -1116,6 +1165,7 @@ impl Account { sub_accounts: HashMap::new(), hardware_wallet_accounts: Vec::new(), canisters: Vec::new(), + imported_tokens: None, } } } diff --git a/rs/backend/src/accounts_store/schema/tests.rs b/rs/backend/src/accounts_store/schema/tests.rs index 13de7db6dd6..01698d57c35 100644 --- a/rs/backend/src/accounts_store/schema/tests.rs +++ b/rs/backend/src/accounts_store/schema/tests.rs @@ -24,6 +24,7 @@ pub fn toy_account(account_index: u64, num_canisters: u64) -> Account { sub_accounts: HashMap::new(), hardware_wallet_accounts: Vec::new(), canisters: Vec::new(), + imported_tokens: None, }; // Attaches canisters to the account. for canister_index in 0..num_canisters { diff --git a/rs/backend/src/accounts_store/tests.rs b/rs/backend/src/accounts_store/tests.rs index 6a299e775b9..b636d6672c0 100644 --- a/rs/backend/src/accounts_store/tests.rs +++ b/rs/backend/src/accounts_store/tests.rs @@ -779,6 +779,62 @@ fn detach_canister_canister_not_found() { assert_eq!(canister_id1, canisters[0].canister_id); } +#[test] +fn set_and_get_imported_tokens() { + let mut store = setup_test_store(); + let principal = PrincipalId::from_str(TEST_ACCOUNT_1).unwrap(); + let ledger_canister_id = PrincipalId::from_str(TEST_ACCOUNT_2).unwrap(); + let index_canister_id = PrincipalId::from_str(TEST_ACCOUNT_3).unwrap(); + + assert_eq!( + store.get_imported_tokens(principal), + GetImportedTokensResponse::Ok(ImportedTokens::default()) + ); + + assert_eq!( + store.set_imported_tokens( + principal, + ImportedTokens { + imported_tokens: vec![ImportedToken { + ledger_canister_id: ledger_canister_id, + index_canister_id: Some(index_canister_id), + }], + }, + ), + SetImportedTokensResponse::Ok + ); + + assert_eq!( + store.get_imported_tokens(principal), + GetImportedTokensResponse::Ok(ImportedTokens { + imported_tokens: vec![ImportedToken { + ledger_canister_id: ledger_canister_id, + index_canister_id: Some(index_canister_id), + }], + }) + ); +} + +#[test] +fn set_imported_tokens_account_not_found() { + let mut store = setup_test_store(); + let non_existing_principal = PrincipalId::from_str(TEST_ACCOUNT_3).unwrap(); + assert_eq!( + store.set_imported_tokens(non_existing_principal, ImportedTokens::default()), + SetImportedTokensResponse::AccountNotFound + ); +} + +#[test] +fn get_imported_tokens_account_not_found() { + let mut store = setup_test_store(); + let non_existing_principal = PrincipalId::from_str(TEST_ACCOUNT_3).unwrap(); + assert_eq!( + store.get_imported_tokens(non_existing_principal), + GetImportedTokensResponse::AccountNotFound + ); +} + #[test] fn sub_account_name_too_long() { let mut store = setup_test_store(); diff --git a/rs/backend/src/accounts_store/toy_data.rs b/rs/backend/src/accounts_store/toy_data.rs index 43f62d2ddb9..ca9a7a630bc 100644 --- a/rs/backend/src/accounts_store/toy_data.rs +++ b/rs/backend/src/accounts_store/toy_data.rs @@ -59,6 +59,7 @@ pub fn toy_account(account_index: u64, size: ToyAccountSize) -> Account { sub_accounts: HashMap::new(), hardware_wallet_accounts: Vec::new(), canisters: Vec::new(), + imported_tokens: None, }; // Creates linked sub-accounts: // Note: Successive accounts have 0, 1, 2 ... MAX_SUB_ACCOUNTS_PER_ACCOUNT-1 sub accounts, restarting at 0. diff --git a/rs/backend/src/main.rs b/rs/backend/src/main.rs index d12f743c065..614fca328cc 100644 --- a/rs/backend/src/main.rs +++ b/rs/backend/src/main.rs @@ -1,8 +1,9 @@ use crate::accounts_store::histogram::AccountsStoreHistogram; use crate::accounts_store::{ AccountDetails, AttachCanisterRequest, AttachCanisterResponse, CreateSubAccountResponse, DetachCanisterRequest, - DetachCanisterResponse, NamedCanister, RegisterHardwareWalletRequest, RegisterHardwareWalletResponse, - RenameCanisterRequest, RenameCanisterResponse, RenameSubAccountRequest, RenameSubAccountResponse, + DetachCanisterResponse, GetImportedTokensResponse, ImportedTokens, NamedCanister, RegisterHardwareWalletRequest, + RegisterHardwareWalletResponse, RenameCanisterRequest, RenameCanisterResponse, RenameSubAccountRequest, + RenameSubAccountResponse, SetImportedTokensResponse, }; use crate::arguments::{set_canister_arguments, CanisterArguments, CANISTER_ARGUMENTS}; use crate::assets::{hash_bytes, insert_asset, insert_tar_xz, Asset}; @@ -238,6 +239,26 @@ fn detach_canister_impl(request: DetachCanisterRequest) -> DetachCanisterRespons STATE.with(|s| s.accounts_store.borrow_mut().detach_canister(principal, request)) } +#[export_name = "canister_update set_imported_tokens"] +pub fn set_imported_tokens() { + over(candid_one, set_imported_tokens_impl); +} + +fn set_imported_tokens_impl(settings: ImportedTokens) -> SetImportedTokensResponse { + let principal = dfn_core::api::caller(); + STATE.with(|s| s.accounts_store.borrow_mut().set_imported_tokens(principal, settings)) +} + +#[export_name = "canister_query get_imported_tokens"] +pub fn get_imported_tokens() { + over(candid_one, |()| get_imported_tokens_impl()); +} + +fn get_imported_tokens_impl() -> GetImportedTokensResponse { + let principal = dfn_core::api::caller(); + STATE.with(|s| s.accounts_store.borrow_mut().get_imported_tokens(principal)) +} + #[export_name = "canister_update get_proposal_payload"] pub fn get_proposal_payload() { over_async(candid_one, proposals::get_proposal_payload); From 0603b11804f4b5adbfd6440ec59c6e6f9a458d4e Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 11:45:47 +0200 Subject: [PATCH 2/9] Add imported tokens types to the candid file --- .../src/lib/canisters/nns-dapp/nns-dapp.did | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/src/lib/canisters/nns-dapp/nns-dapp.did b/frontend/src/lib/canisters/nns-dapp/nns-dapp.did index 11eb49e67a4..a68a16016ba 100644 --- a/frontend/src/lib/canisters/nns-dapp/nns-dapp.did +++ b/frontend/src/lib/canisters/nns-dapp/nns-dapp.did @@ -155,6 +155,27 @@ type HttpResponse = body: blob; }; +type ImportedToken = record { + ledger_canister_id: principal; + index_canister_id: opt principal; +}; + +type ImportedTokens = record { + imported_tokens: vec ImportedToken; +}; + +type SetImportedTokensResponse = + variant { + Ok; + AccountNotFound; + }; + +type GetImportedTokensResponse = + variant { + Ok: ImportedTokens; + AccountNotFound; + }; + service : { get_account: () -> (GetAccountResponse) query; add_account: () -> (AccountIdentifier); @@ -170,4 +191,7 @@ service : { http_request: (request: HttpRequest) -> (HttpResponse) query; add_stable_asset: (asset: blob) -> (); + + set_imported_tokens: (ImportedTokens) -> (SetImportedTokensResponse); + get_imported_tokens: () -> (GetImportedTokensResponse) query; } From 837ad6442fc9341a86930c8c8d8915a689928250 Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 11:46:30 +0200 Subject: [PATCH 3/9] Update js/ts types --- .../nns-dapp/nns-dapp.certified.idl.js | 21 +++++++++++++++++++ .../lib/canisters/nns-dapp/nns-dapp.types.ts | 17 +++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/frontend/src/lib/canisters/nns-dapp/nns-dapp.certified.idl.js b/frontend/src/lib/canisters/nns-dapp/nns-dapp.certified.idl.js index 86bb0a88889..bca944134b8 100644 --- a/frontend/src/lib/canisters/nns-dapp/nns-dapp.certified.idl.js +++ b/frontend/src/lib/canisters/nns-dapp/nns-dapp.certified.idl.js @@ -48,6 +48,17 @@ export const idlFactory = ({ IDL }) => { name: IDL.Text, canister_id: IDL.Principal, }); + const ImportedToken = IDL.Record({ + index_canister_id: IDL.Opt(IDL.Principal), + ledger_canister_id: IDL.Principal, + }); + const ImportedTokens = IDL.Record({ + imported_tokens: IDL.Vec(ImportedToken), + }); + const GetImportedTokensResponse = IDL.Variant({ + Ok: ImportedTokens, + AccountNotFound: IDL.Null, + }); const BlockHeight = IDL.Nat64; const NeuronId = IDL.Nat64; const GetProposalPayloadResponse = IDL.Variant({ @@ -108,6 +119,10 @@ export const idlFactory = ({ IDL }) => { SubAccountNotFound: IDL.Null, NameTooLong: IDL.Null, }); + const SetImportedTokensResponse = IDL.Variant({ + Ok: IDL.Null, + AccountNotFound: IDL.Null, + }); return IDL.Service({ add_account: IDL.Func([], [AccountIdentifier], []), add_stable_asset: IDL.Func([IDL.Vec(IDL.Nat8)], [], []), @@ -129,6 +144,7 @@ export const idlFactory = ({ IDL }) => { ), get_account: IDL.Func([], [GetAccountResponse], []), get_canisters: IDL.Func([], [IDL.Vec(CanisterDetails)], []), + get_imported_tokens: IDL.Func([], [GetImportedTokensResponse], []), get_proposal_payload: IDL.Func( [IDL.Nat64], [GetProposalPayloadResponse], @@ -146,6 +162,11 @@ export const idlFactory = ({ IDL }) => { [RenameSubAccountResponse], [] ), + set_imported_tokens: IDL.Func( + [ImportedTokens], + [SetImportedTokensResponse], + [] + ), }); }; // Remove param `{ IDL }` because TS was complaining of unused variable diff --git a/frontend/src/lib/canisters/nns-dapp/nns-dapp.types.ts b/frontend/src/lib/canisters/nns-dapp/nns-dapp.types.ts index 39bf849783f..4b560460d7c 100644 --- a/frontend/src/lib/canisters/nns-dapp/nns-dapp.types.ts +++ b/frontend/src/lib/canisters/nns-dapp/nns-dapp.types.ts @@ -35,6 +35,9 @@ export type DetachCanisterResponse = { Ok: null } | { CanisterNotFound: null }; export type GetAccountResponse = | { Ok: AccountDetails } | { AccountNotFound: null }; +export type GetImportedTokensResponse = + | { Ok: ImportedTokens } + | { AccountNotFound: null }; export type GetProposalPayloadResponse = { Ok: string } | { Err: string }; export interface HardwareWalletAccountDetails { principal: Principal; @@ -53,6 +56,13 @@ export interface HttpResponse { headers: Array; status_code: number; } +export interface ImportedToken { + index_canister_id: [] | [Principal]; + ledger_canister_id: Principal; +} +export interface ImportedTokens { + imported_tokens: Array; +} export interface RegisterHardwareWalletRequest { principal: Principal; name: string; @@ -82,6 +92,9 @@ export type RenameSubAccountResponse = | { AccountNotFound: null } | { SubAccountNotFound: null } | { NameTooLong: null }; +export type SetImportedTokensResponse = + | { Ok: null } + | { AccountNotFound: null }; export interface Stats { seconds_since_last_ledger_sync: bigint; sub_accounts_count: bigint; @@ -115,6 +128,7 @@ export default interface _SERVICE { ) => Promise; get_account: () => Promise; get_canisters: () => Promise>; + get_imported_tokens: () => Promise; get_proposal_payload: (arg_0: bigint) => Promise; get_stats: () => Promise; http_request: (arg_0: HttpRequest) => Promise; @@ -124,4 +138,7 @@ export default interface _SERVICE { rename_sub_account: ( arg_0: RenameSubAccountRequest ) => Promise; + set_imported_tokens: ( + arg_0: ImportedTokens + ) => Promise; } From 1e4b8b4a22042de356d2e5262127c93bce259945 Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 11:47:26 +0200 Subject: [PATCH 4/9] Add imported tokens api --- frontend/src/lib/api/canisters.api.ts | 32 +++++++++++++++++++ .../canisters/nns-dapp/nns-dapp.canister.ts | 28 +++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/api/canisters.api.ts b/frontend/src/lib/api/canisters.api.ts index 4019c28e20e..461130ec62a 100644 --- a/frontend/src/lib/api/canisters.api.ts +++ b/frontend/src/lib/api/canisters.api.ts @@ -11,6 +11,8 @@ import { } from "$lib/canisters/nns-dapp/nns-dapp.errors"; import type { CanisterDetails as CanisterInfo, + ImportedToken, + ImportedTokens, SubAccountArray, } from "$lib/canisters/nns-dapp/nns-dapp.types"; import { @@ -376,3 +378,33 @@ const canisters = async ( return { cmc, icMgt, nnsDapp }; }; + +export const getImportedTokens = async ({ + identity, +}: { + identity: Identity; +}): Promise => { + logWithTimestamp("Getting imported tokens call..."); + const { nnsDapp } = await canisters(identity); + + const importedTokens = await nnsDapp.getImportedTokens(); + + logWithTimestamp("Getting imported tokens call complete."); + + return importedTokens; +}; + +export const setImportedTokens = async ({ + identity, + importedTokens, +}: { + identity: Identity; + importedTokens: Array; +}): Promise => { + logWithTimestamp("Setting imported tokens call..."); + const { nnsDapp } = await canisters(identity); + + await nnsDapp.setImportedTokens(importedTokens); + + logWithTimestamp("Setting imported tokens call complete."); +}; diff --git a/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts b/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts index 1efe86dd3c3..019539d800f 100644 --- a/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts +++ b/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts @@ -25,7 +25,7 @@ import type { AccountDetails, CanisterDetails, CreateSubAccountResponse, - GetAccountResponse, + GetAccountResponse, ImportedToken, ImportedTokens, RegisterHardwareWalletRequest, RegisterHardwareWalletResponse, RenameSubAccountRequest, @@ -325,4 +325,30 @@ export class NNSDappCanister { errorText ?? (nonNullish(response) ? JSON.stringify(response) : undefined) ); } + + public getImportedTokens = async (): Promise => { + const response = await this.certifiedService.get_imported_tokens(); + if ("Ok" in response) { + return response.Ok; + } + if ("AccountNotFound" in response) { + throw new AccountNotFoundError("error__account.not_found"); + } + // Edge case + throw new Error(`Error getting imported tokens ${JSON.stringify(response)}`); + }; + + public setImportedTokens = async (importedTokens: Array): Promise => { + const response = await this.certifiedService.set_imported_tokens( + { 'imported_tokens' : importedTokens } + ); + if ("Ok" in response) { + return; + } + if ("AccountNotFound" in response) { + throw new AccountNotFoundError("error__account.not_found"); + } + // Edge case + throw new Error(`Error setting imported tokens ${JSON.stringify(response)}`); + }; } From 154c6ee0c902c235de61e62e9e4e051af12e9a24 Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 12:09:42 +0200 Subject: [PATCH 5/9] Formatting --- .../canisters/nns-dapp/nns-dapp.canister.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts b/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts index 019539d800f..eac7ac807ab 100644 --- a/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts +++ b/frontend/src/lib/canisters/nns-dapp/nns-dapp.canister.ts @@ -25,7 +25,9 @@ import type { AccountDetails, CanisterDetails, CreateSubAccountResponse, - GetAccountResponse, ImportedToken, ImportedTokens, + GetAccountResponse, + ImportedToken, + ImportedTokens, RegisterHardwareWalletRequest, RegisterHardwareWalletResponse, RenameSubAccountRequest, @@ -335,13 +337,17 @@ export class NNSDappCanister { throw new AccountNotFoundError("error__account.not_found"); } // Edge case - throw new Error(`Error getting imported tokens ${JSON.stringify(response)}`); + throw new Error( + `Error getting imported tokens ${JSON.stringify(response)}` + ); }; - public setImportedTokens = async (importedTokens: Array): Promise => { - const response = await this.certifiedService.set_imported_tokens( - { 'imported_tokens' : importedTokens } - ); + public setImportedTokens = async ( + importedTokens: Array + ): Promise => { + const response = await this.certifiedService.set_imported_tokens({ + imported_tokens: importedTokens, + }); if ("Ok" in response) { return; } @@ -349,6 +355,8 @@ export class NNSDappCanister { throw new AccountNotFoundError("error__account.not_found"); } // Edge case - throw new Error(`Error setting imported tokens ${JSON.stringify(response)}`); + throw new Error( + `Error setting imported tokens ${JSON.stringify(response)}` + ); }; } From a6b18d449cb86a9a82e3d2734e919e5466f3b215 Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 13:58:40 +0200 Subject: [PATCH 6/9] Add mockImportedToken --- frontend/src/tests/mocks/icp-accounts.store.mock.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/tests/mocks/icp-accounts.store.mock.ts b/frontend/src/tests/mocks/icp-accounts.store.mock.ts index df7464318f6..d4a660e9a13 100644 --- a/frontend/src/tests/mocks/icp-accounts.store.mock.ts +++ b/frontend/src/tests/mocks/icp-accounts.store.mock.ts @@ -1,10 +1,15 @@ import type { AccountDetails, HardwareWalletAccountDetails, + ImportedToken, SubAccountDetails, } from "$lib/canisters/nns-dapp/nns-dapp.types"; import type { IcpAccountsStoreData } from "$lib/derived/icp-accounts.derived"; import type { Account } from "$lib/types/account"; +import { + indexCanisterIdMock, + ledgerCanisterIdMock, +} from "$tests/mocks/sns.api.mock"; import { Principal } from "@dfinity/principal"; import type { Subscriber } from "svelte/store"; @@ -84,3 +89,8 @@ export const mockAccountsStoreSubscribe = export const mockAddressInputValid = "cd70bfa0f092c38a0ff8643d4617219761eb61d199b15418c0b1114d59e30f8e"; export const mockAddressInputInvalid = "not-valid"; + +export const mockImportedToken: ImportedToken = { + ledger_canister_id: ledgerCanisterIdMock, + index_canister_id: [indexCanisterIdMock], +}; From 15d00535b03af61c9855f28c510aa012bd899929 Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 13:58:50 +0200 Subject: [PATCH 7/9] Test NNSDapp.getImportedTokens --- .../lib/canisters/nns-dapp.canister.spec.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts b/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts index 861a880268c..4433c8b1a5d 100644 --- a/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts +++ b/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts @@ -17,11 +17,13 @@ import type { NNSDappService } from "$lib/canisters/nns-dapp/nns-dapp.idl"; import type { CreateSubAccountResponse, GetAccountResponse, + GetImportedTokensResponse, } from "$lib/canisters/nns-dapp/nns-dapp.types"; import { mockPrincipal } from "$tests/mocks/auth.store.mock"; import { mockCanister, mockCanisters } from "$tests/mocks/canisters.mock"; import { mockAccountDetails, + mockImportedToken, mockSubAccountDetails, } from "$tests/mocks/icp-accounts.store.mock"; import type { HttpAgent } from "@dfinity/agent"; @@ -473,4 +475,51 @@ describe("NNSDapp", () => { expect(call).rejects.toThrowError(UnknownProposalPayloadError); }); + + describe("NNSDapp.getImportedTokens", () => { + it("should call get_imported_tokens", async () => { + const service = mock(); + service.get_imported_tokens.mockResolvedValue({ + Ok: { + imported_tokens: [], + }, + }); + const nnsDapp = await createNnsDapp(service); + + expect(service.get_imported_tokens).not.toBeCalled(); + + await nnsDapp.getImportedTokens(); + + expect(service.get_imported_tokens).toBeCalledTimes(1); + }); + + it("should return imported tokens", async () => { + const service = mock(); + service.get_imported_tokens.mockResolvedValue({ + Ok: { + imported_tokens: [mockImportedToken], + }, + }); + const nnsDapp = await createNnsDapp(service); + const result = await nnsDapp.getImportedTokens(); + + expect(result).toEqual({ + imported_tokens: [mockImportedToken], + }); + }); + + it("throws error if account not found", async () => { + const response: GetImportedTokensResponse = { + AccountNotFound: null, + }; + const service = mock(); + service.get_imported_tokens.mockResolvedValue(response); + + const nnsDapp = await createNnsDapp(service); + + const call = async () => nnsDapp.getImportedTokens(); + + await expect(call).rejects.toThrow(AccountNotFoundError); + }); + }); }); From a8e3d2c95bd6dc11f9821ff2364d8e1041ec94d6 Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 16:15:40 +0200 Subject: [PATCH 8/9] Tests for NNSDapp.setImportedTokens --- .../lib/canisters/nns-dapp.canister.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts b/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts index 4433c8b1a5d..28f635b0222 100644 --- a/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts +++ b/frontend/src/tests/lib/canisters/nns-dapp.canister.spec.ts @@ -18,6 +18,7 @@ import type { CreateSubAccountResponse, GetAccountResponse, GetImportedTokensResponse, + SetImportedTokensResponse, } from "$lib/canisters/nns-dapp/nns-dapp.types"; import { mockPrincipal } from "$tests/mocks/auth.store.mock"; import { mockCanister, mockCanisters } from "$tests/mocks/canisters.mock"; @@ -521,5 +522,74 @@ describe("NNSDapp", () => { await expect(call).rejects.toThrow(AccountNotFoundError); }); + + it("should provide generic error message", async () => { + const response = { + UnexpectedError: "message", + }; + const service = mock(); + service.get_imported_tokens.mockResolvedValue( + response as unknown as GetImportedTokensResponse + ); + + const nnsDapp = await createNnsDapp(service); + + const call = async () => nnsDapp.getImportedTokens(); + + await expect(call).rejects.toThrow( + 'Error getting imported tokens {"UnexpectedError":"message"}' + ); + }); + }); + + describe("NNSDapp.setImportedTokens", () => { + it("should call set_imported_tokens", async () => { + const service = mock(); + service.set_imported_tokens.mockResolvedValue({ + Ok: null, + }); + const nnsDapp = await createNnsDapp(service); + + expect(service.set_imported_tokens).not.toBeCalled(); + + await nnsDapp.setImportedTokens([mockImportedToken]); + + expect(service.set_imported_tokens).toBeCalledTimes(1); + expect(service.set_imported_tokens).toBeCalledWith({ + imported_tokens: [mockImportedToken], + }); + }); + + it("throws error if account not found", async () => { + const response: SetImportedTokensResponse = { + AccountNotFound: null, + }; + const service = mock(); + service.set_imported_tokens.mockResolvedValue(response); + + const nnsDapp = await createNnsDapp(service); + + const call = async () => nnsDapp.setImportedTokens([]); + + await expect(call).rejects.toThrow(AccountNotFoundError); + }); + + it("should provide generic error message", async () => { + const response = { + UnexpectedError: "message", + }; + const service = mock(); + service.set_imported_tokens.mockResolvedValue( + response as unknown as SetImportedTokensResponse + ); + + const nnsDapp = await createNnsDapp(service); + + const call = async () => nnsDapp.setImportedTokens([]); + + await expect(call).rejects.toThrow( + 'Error setting imported tokens {"UnexpectedError":"message"}' + ); + }); }); }); From d914ee4fee5c1916771eb2fbcbfc27f2596b8376 Mon Sep 17 00:00:00 2001 From: Max Strasinsky Date: Fri, 19 Jul 2024 16:49:29 +0200 Subject: [PATCH 9/9] Test imported token api --- .../src/tests/lib/api/canisters.api.spec.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/tests/lib/api/canisters.api.spec.ts b/frontend/src/tests/lib/api/canisters.api.spec.ts index 6619176b1ca..d9aa4cb6882 100644 --- a/frontend/src/tests/lib/api/canisters.api.spec.ts +++ b/frontend/src/tests/lib/api/canisters.api.spec.ts @@ -3,9 +3,11 @@ import { createCanister, detachCanister, getIcpToCyclesExchangeRate, + getImportedTokens, queryCanisterDetails, queryCanisters, renameCanister, + setImportedTokens, topUpCanister, updateSettings, } from "$lib/api/canisters.api"; @@ -24,7 +26,10 @@ import { mockCanisterDetails, mockCanisterSettings, } from "$tests/mocks/canisters.mock"; -import { mockSubAccount } from "$tests/mocks/icp-accounts.store.mock"; +import { + mockImportedToken, + mockSubAccount, +} from "$tests/mocks/icp-accounts.store.mock"; import { CMCCanister, ProcessingError } from "@dfinity/cmc"; import { AccountIdentifier, @@ -431,4 +436,30 @@ describe("canisters-api", () => { expect(mockCMCCanister.notifyTopUp).not.toBeCalled(); }); }); + + describe("getImportedTokens", () => { + it("should call the nns dapp canister to get the imported tokens", async () => { + expect(mockNNSDappCanister.getImportedTokens).not.toBeCalled(); + await getImportedTokens({ + identity: mockIdentity, + }); + + expect(mockNNSDappCanister.getImportedTokens).toBeCalledTimes(1); + }); + }); + + describe("setImportedTokens", () => { + it("should call the nns dapp canister to set imported tokens", async () => { + expect(mockNNSDappCanister.setImportedTokens).not.toBeCalled(); + await setImportedTokens({ + identity: mockIdentity, + importedTokens: [mockImportedToken], + }); + + expect(mockNNSDappCanister.setImportedTokens).toBeCalledTimes(1); + expect(mockNNSDappCanister.setImportedTokens).toBeCalledWith([ + mockImportedToken, + ]); + }); + }); });