From b4f39a8d49dc22a8813019ef03c8514fdec22e1b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 07:13:43 +0100 Subject: [PATCH 01/24] feat(frontend): ic token zod --- src/frontend/src/icp/types/ic.ts | 77 +++++++++++++++---------- src/frontend/src/lib/types/canister.ts | 5 +- src/frontend/src/lib/types/coingecko.ts | 5 +- src/frontend/src/lib/types/token.ts | 2 +- 4 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/frontend/src/icp/types/ic.ts b/src/frontend/src/icp/types/ic.ts index 65e2439527..cc841cd899 100644 --- a/src/frontend/src/icp/types/ic.ts +++ b/src/frontend/src/icp/types/ic.ts @@ -1,17 +1,15 @@ -import type { - IndexCanisterIdText, - LedgerCanisterIdText, - MinterCanisterIdText -} from '$icp/types/canister'; -import type { CoingeckoCoinsId } from '$lib/types/coingecko'; -import type { Token } from '$lib/types/token'; +import { CanisterIdTextSchema } from '$lib/types/canister'; +import { CoingeckoCoinsIdSchema } from '$lib/types/coingecko'; +import { TokenSchema } from '$lib/types/token'; import type { TransactionType } from '$lib/types/transaction'; import type { Option } from '$lib/types/utils'; +import { UrlSchema } from '$lib/validation/url.validation'; import type { Transaction, TransactionWithId } from '@dfinity/ledger-icp'; import type { IcrcTransaction as IcrcTransactionCandid, IcrcTransactionWithId } from '@dfinity/ledger-icrc'; +import { z } from 'zod'; export interface IcTransactionAddOnsInfo { transferToSelf?: 'send' | 'receive'; @@ -53,37 +51,54 @@ export interface IcTransactionUi { txExplorerUrl?: string; } -export type IcToken = Token & IcFee & IcInterface; -export type IcTokenWithoutId = Omit; +const IcFeeSchema = z.object({ + fee: z.bigint() +}); -export interface IcFee { - fee: bigint; -} +export type IcFee = z.infer; -export type IcInterface = IcCanisters & IcAppMetadata; -export interface IcCanisters { - ledgerCanisterId: LedgerCanisterIdText; - indexCanisterId: IndexCanisterIdText; -} +const IcAppMetadataSchema = z.object({ + exchangeCoinId: CoingeckoCoinsIdSchema.optional(), + position: z.number(), + explorerUrl: UrlSchema.optional() +}); -export type IcCkToken = IcToken & Partial; +export type IcAppMetadata = z.infer; -export type IcCkInterface = IcInterface & IcCkMetadata; +const IcCanistersSchema = z.object({ + ledgerCanisterId: CanisterIdTextSchema, + indexCanisterId: CanisterIdTextSchema +}); -export type IcCkMetadata = { - minterCanisterId: MinterCanisterIdText; -} & Partial; +export type IcCanisters = z.infer; -export interface IcCkLinkedAssets { - twinToken: Token; - feeLedgerCanisterId?: LedgerCanisterIdText; -} +const IcCkLinkedAssetsSchema = z.object({ + twinToken: TokenSchema, + feeLedgerCanisterId: CanisterIdTextSchema.optional() +}); -export interface IcAppMetadata { - exchangeCoinId?: CoingeckoCoinsId; - position: number; - explorerUrl?: string; -} +export type IcCkLinkedAssets = z.infer; + +const IcCkMetadataSchema = IcCkLinkedAssetsSchema.partial().extend({ + minterCanisterId: CanisterIdTextSchema +}); + +export type IcCkMetadata = z.infer; + +const IcInterfaceSchema = IcCanistersSchema.merge(IcAppMetadataSchema); +export type IcInterface = z.infer; + +const IcTokenSchema = TokenSchema.merge(IcFeeSchema).merge(IcInterfaceSchema); +export type IcToken = z.infer; + +const IcTokenWithoutIdSchema = IcTokenSchema.omit({ id: true }); +export type IcTokenWithoutId = z.infer; + +const IcCkTokenSchema = IcTokenSchema.merge(IcCkMetadataSchema.partial()); +export type IcCkToken = z.infer; + +const IcCkInterfaceSchema = IcInterfaceSchema.merge(IcCkMetadataSchema); +export type IcCkInterface = z.infer; export type OptionIcToken = Option; export type OptionIcCkToken = Option; diff --git a/src/frontend/src/lib/types/canister.ts b/src/frontend/src/lib/types/canister.ts index 48ad651bc1..51f8b3678a 100644 --- a/src/frontend/src/lib/types/canister.ts +++ b/src/frontend/src/lib/types/canister.ts @@ -3,8 +3,11 @@ import type { Option } from '$lib/types/utils'; import type { Identity } from '@dfinity/agent'; import { Principal } from '@dfinity/principal'; import type { CanisterOptions } from '@dfinity/utils'; +import { z } from 'zod'; -export type CanisterIdText = string; +export const CanisterIdTextSchema = z.string(); + +export type CanisterIdText = z.infer; export type OptionCanisterIdText = Option; diff --git a/src/frontend/src/lib/types/coingecko.ts b/src/frontend/src/lib/types/coingecko.ts index 18e2bb7d90..ad999100cb 100644 --- a/src/frontend/src/lib/types/coingecko.ts +++ b/src/frontend/src/lib/types/coingecko.ts @@ -4,8 +4,11 @@ // *refers to curl -l https://api.coingecko.com/api/v3/coins/list import type { LedgerCanisterIdText } from '$icp/types/canister'; import type { EthAddress } from '$lib/types/address'; +import { z } from 'zod'; -export type CoingeckoCoinsId = 'ethereum' | 'bitcoin' | 'internet-computer'; +export const CoingeckoCoinsIdSchema = z.enum(['ethereum', 'bitcoin', 'internet-computer']); + +export type CoingeckoCoinsId = z.infer; // We are interested only in the ERC20 <> USD on Ethereum and in the ICRC <> USD on Internet Computer, therefore not an exhaustive list. // *refers to curl -l https://api.coingecko.com/api/v3/asset_platforms diff --git a/src/frontend/src/lib/types/token.ts b/src/frontend/src/lib/types/token.ts index 36e7402f2f..6dbb13fe81 100644 --- a/src/frontend/src/lib/types/token.ts +++ b/src/frontend/src/lib/types/token.ts @@ -40,7 +40,7 @@ const TokenBuyableSchema = z.object({ buy: z.custom>().optional() }); -const TokenSchema = z +export const TokenSchema = z .object({ id: TokenIdSchema, network: NetworkSchema, From 4de89ddbe6ef69bc1e60bb30644f5bad8eb87dad Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 08:14:05 +0100 Subject: [PATCH 02/24] feat: validate data --- src/frontend/src/icp/types/ic.ts | 45 +-- .../src/icp/validation/ic-token.validation.ts | 39 +++ .../validation/ic-token.validation.spec.ts | 330 ++++++++++++++++++ 3 files changed, 381 insertions(+), 33 deletions(-) create mode 100644 src/frontend/src/icp/validation/ic-token.validation.ts create mode 100644 src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts diff --git a/src/frontend/src/icp/types/ic.ts b/src/frontend/src/icp/types/ic.ts index cc841cd899..6e8b52edcd 100644 --- a/src/frontend/src/icp/types/ic.ts +++ b/src/frontend/src/icp/types/ic.ts @@ -1,9 +1,17 @@ -import { CanisterIdTextSchema } from '$lib/types/canister'; -import { CoingeckoCoinsIdSchema } from '$lib/types/coingecko'; -import { TokenSchema } from '$lib/types/token'; +import { + IcAppMetadataSchema, + IcCanistersSchema, + IcCkInterfaceSchema, + IcCkLinkedAssetsSchema, + IcCkMetadataSchema, + IcCkTokenSchema, + IcFeeSchema, + IcInterfaceSchema, + IcTokenSchema, + IcTokenWithoutIdSchema +} from '$icp/validation/ic-token.validation'; import type { TransactionType } from '$lib/types/transaction'; import type { Option } from '$lib/types/utils'; -import { UrlSchema } from '$lib/validation/url.validation'; import type { Transaction, TransactionWithId } from '@dfinity/ledger-icp'; import type { IcrcTransaction as IcrcTransactionCandid, @@ -51,53 +59,24 @@ export interface IcTransactionUi { txExplorerUrl?: string; } -const IcFeeSchema = z.object({ - fee: z.bigint() -}); - export type IcFee = z.infer; -const IcAppMetadataSchema = z.object({ - exchangeCoinId: CoingeckoCoinsIdSchema.optional(), - position: z.number(), - explorerUrl: UrlSchema.optional() -}); - export type IcAppMetadata = z.infer; -const IcCanistersSchema = z.object({ - ledgerCanisterId: CanisterIdTextSchema, - indexCanisterId: CanisterIdTextSchema -}); - export type IcCanisters = z.infer; -const IcCkLinkedAssetsSchema = z.object({ - twinToken: TokenSchema, - feeLedgerCanisterId: CanisterIdTextSchema.optional() -}); - export type IcCkLinkedAssets = z.infer; -const IcCkMetadataSchema = IcCkLinkedAssetsSchema.partial().extend({ - minterCanisterId: CanisterIdTextSchema -}); - export type IcCkMetadata = z.infer; -const IcInterfaceSchema = IcCanistersSchema.merge(IcAppMetadataSchema); export type IcInterface = z.infer; -const IcTokenSchema = TokenSchema.merge(IcFeeSchema).merge(IcInterfaceSchema); export type IcToken = z.infer; -const IcTokenWithoutIdSchema = IcTokenSchema.omit({ id: true }); export type IcTokenWithoutId = z.infer; -const IcCkTokenSchema = IcTokenSchema.merge(IcCkMetadataSchema.partial()); export type IcCkToken = z.infer; -const IcCkInterfaceSchema = IcInterfaceSchema.merge(IcCkMetadataSchema); export type IcCkInterface = z.infer; export type OptionIcToken = Option; diff --git a/src/frontend/src/icp/validation/ic-token.validation.ts b/src/frontend/src/icp/validation/ic-token.validation.ts new file mode 100644 index 0000000000..43a38ce3f3 --- /dev/null +++ b/src/frontend/src/icp/validation/ic-token.validation.ts @@ -0,0 +1,39 @@ +import { CanisterIdTextSchema } from '$lib/types/canister'; +import { CoingeckoCoinsIdSchema } from '$lib/types/coingecko'; +import { TokenSchema } from '$lib/types/token'; +import { UrlSchema } from '$lib/validation/url.validation'; +import { z } from 'zod'; + +export const IcFeeSchema = z.object({ + fee: z.bigint() +}); + +export const IcAppMetadataSchema = z.object({ + exchangeCoinId: CoingeckoCoinsIdSchema.optional(), + position: z.number(), + explorerUrl: UrlSchema.optional() +}); + +export const IcCanistersSchema = z.object({ + ledgerCanisterId: CanisterIdTextSchema, + indexCanisterId: CanisterIdTextSchema +}); + +export const IcCkLinkedAssetsSchema = z.object({ + twinToken: TokenSchema, + feeLedgerCanisterId: CanisterIdTextSchema.optional() +}); + +export const IcCkMetadataSchema = IcCkLinkedAssetsSchema.partial().extend({ + minterCanisterId: CanisterIdTextSchema +}); + +export const IcInterfaceSchema = IcCanistersSchema.merge(IcAppMetadataSchema); + +export const IcTokenSchema = TokenSchema.merge(IcFeeSchema).merge(IcInterfaceSchema); + +export const IcTokenWithoutIdSchema = IcTokenSchema.omit({ id: true }).strict(); + +export const IcCkTokenSchema = IcTokenSchema.merge(IcCkMetadataSchema.partial()); + +export const IcCkInterfaceSchema = IcInterfaceSchema.merge(IcCkMetadataSchema); diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts new file mode 100644 index 0000000000..4fdb32f3a5 --- /dev/null +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -0,0 +1,330 @@ +import { SEPOLIA_NETWORK } from '$env/networks.env'; +import { + IC_CKBTC_INDEX_CANISTER_ID, + IC_CKBTC_LEDGER_CANISTER_ID, + IC_CKBTC_MINTER_CANISTER_ID +} from '$env/networks.icrc.env'; +import { + IcAppMetadataSchema, + IcCanistersSchema, + IcCkInterfaceSchema, + IcCkLinkedAssetsSchema, + IcCkMetadataSchema, + IcCkTokenSchema, + IcFeeSchema, + IcInterfaceSchema, + IcTokenSchema, + IcTokenWithoutIdSchema +} from '$icp/validation/ic-token.validation'; +import { parseTokenId } from '$lib/validation/token.validation'; + +describe('Schema Validation Tests', () => { + const { chainId: _, explorerUrl: __, ...mockNetwork } = SEPOLIA_NETWORK; + + const mockToken = { + id: parseTokenId('Test'), + network: mockNetwork, + standard: 'icp', + category: 'default', + name: 'SampleToken', + symbol: 'STK', + decimals: 8, + oisySymbol: { oisySymbol: 'OSYM' }, + oisyName: { prefix: 'OS', oisyName: 'OisyToken' }, + buy: { onramperId: 'onramper-id' } + }; + + const mockFee = { fee: 1000n }; + + const mockCanisters = { + ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID, + indexCanisterId: IC_CKBTC_INDEX_CANISTER_ID + }; + + const mockApp = { + position: 1, + exchangeCoinId: 'bitcoin', + explorerUrl: 'https://explorer.example.com' + }; + + describe('IcFeeSchema', () => { + it('should validate with correct data', () => { + const validData = mockFee; + expect(IcFeeSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with invalid fee type', () => { + const invalidData = { fee: 1000 }; + expect(() => IcFeeSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcAppMetadataSchema', () => { + const validData = mockApp; + + it('should validate with correct data', () => { + expect(IcAppMetadataSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with invalid position', () => { + const invalidData = { + ...validData, + position: 'first' + }; + expect(() => IcAppMetadataSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with invalid explorerUrl', () => { + const invalidData = { + ...validData, + explorerUrl: 'http://localhost:8080' + }; + expect(() => IcAppMetadataSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with invalid exchangeCoinId', () => { + const invalidData = { + ...validData, + exchangeCoinId: 'test' + }; + expect(() => IcAppMetadataSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcCanistersSchema', () => { + const validData = mockCanisters; + + it('should validate with correct data', () => { + expect(IcCanistersSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with invalid ledger canister id', () => { + const invalidData = { + ...validData, + ledgerCanisterId: 'abc' + }; + expect(() => IcCanistersSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with invalid index canister id', () => { + const invalidData = { + ...validData, + indexCanisterId: 'abc' + }; + expect(() => IcCanistersSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with missing ledger canister field', () => { + const invalidData = { + indexCanisterId: IC_CKBTC_INDEX_CANISTER_ID + }; + expect(() => IcCanistersSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with missing index canister field', () => { + const invalidData = { + ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID + }; + expect(() => IcCanistersSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcCkLinkedAssetsSchema', () => { + const validData = { + twinToken: mockToken, + feeLedgerCanisterId: 'doked-biaaa-aaaar-qag2a-cai' + }; + + it('should validate with correct data', () => { + expect(IcCkLinkedAssetsSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with invalid twinToken', () => { + const invalidData = { + ...validData, + twinToken: { id: 'not-a-symbol' } + }; + expect(() => IcCkLinkedAssetsSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with invalid feeLedgerCanisterId', () => { + const invalidData = { + ...validData, + feeLedgerCanisterId: 123 + }; + expect(() => IcCkLinkedAssetsSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcCkMetadataSchema', () => { + const validData = { + twinToken: mockToken, + feeLedgerCanisterId: 'doked-biaaa-aaaar-qag2a-cai', + minterCanisterId: IC_CKBTC_MINTER_CANISTER_ID + }; + + it('should validate with correct data', () => { + expect(IcCkMetadataSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with missing minterCanisterId', () => { + const { minterCanisterId: _, ...invalidData } = validData; + expect(() => IcCkMetadataSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcInterfaceSchema', () => { + const validData = { + ...mockCanisters, + ...mockApp + }; + + it('should validate with correct data', () => { + expect(IcInterfaceSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with incorrect IcCanisters data', () => { + const invalidData = { + ...validData, + ledgerCanisterId: 123 + }; + expect(() => IcInterfaceSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with incorrect IcAppMetadataSchema data', () => { + const invalidData = { + ...validData, + position: 'first' + }; + expect(() => IcInterfaceSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcTokenSchema', () => { + const validData = { + ...mockToken, + ...mockFee, + ...mockCanisters, + ...mockApp + }; + + it('should validate with all required fields', () => { + expect(IcTokenSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with invalid token', () => { + const invalidData = { + ...validData, + id: 'not-a-symbol' + }; + expect(() => IcTokenSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with invalid fee', () => { + const invalidData = { + ...validData, + fee: 1000 + }; + expect(() => IcTokenSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with invalid canister', () => { + const invalidData = { + ...validData, + feeLedgerCanisterId: 123 + }; + expect(() => IcTokenSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with invalid app data', () => { + const invalidData = { + ...validData, + position: 'first' + }; + expect(() => IcTokenSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcTokenWithoutIdSchema', () => { + const { id, ...restToken } = mockToken; + + const validData = { + ...restToken, + ...mockFee, + ...mockCanisters, + ...mockApp + }; + + it('should validate without id field', () => { + expect(IcTokenWithoutIdSchema.parse(validData)).toEqual(validData); + }); + + it('should fail if id field is present', () => { + const invalidData = { + ...validData, + id + }; + expect(() => IcTokenWithoutIdSchema.parse(invalidData)).toThrow(); + }); + }); + + describe('IcCkTokenSchema', () => { + const mockIcToken = { + ...mockToken, + ...mockFee, + ...mockCanisters, + ...mockApp + }; + + it('should validate without IcCkMetadata', () => { + const validData = { + ...mockIcToken + }; + expect(IcCkTokenSchema.parse(validData)).toEqual(validData); + }); + + it('should validate with IcCkMetadata', () => { + const validData = { + ...mockIcToken, + twinToken: mockToken, + minterCanisterId: IC_CKBTC_MINTER_CANISTER_ID + }; + expect(IcCkTokenSchema.parse(validData)).toEqual(validData); + }); + + it('should validate with partial IcCkMetadata', () => { + const validData = { + ...mockIcToken, + twinToken: mockToken + }; + expect(IcCkTokenSchema.parse(validData)).toEqual(validData); + }); + }); + + describe('IcCkInterfaceSchema', () => { + const validData = { + ...mockCanisters, + ...mockApp, + twinToken: mockToken, + feeLedgerCanisterId: 'doked-biaaa-aaaar-qag2a-cai', + minterCanisterId: IC_CKBTC_MINTER_CANISTER_ID + }; + + it('should validate with IcInterface and IcCkMetadata fields', () => { + expect(IcCkInterfaceSchema.parse(validData)).toEqual(validData); + }); + + it('should fail with incorrect IcInterface data', () => { + const invalidData = { + ...validData, + ledgerCanisterId: 123 + }; + expect(() => IcCkInterfaceSchema.parse(invalidData)).toThrow(); + }); + + it('should fail with incorrect IcCkMetadataSchema data', () => { + const { minterCanisterId: _, ...invalidData } = validData; + expect(() => IcCkInterfaceSchema.parse(invalidData)).toThrow(); + }); + }); +}); From e9caffdc391005d328ee869a913ecaf4a749a7ea Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 09:36:48 +0100 Subject: [PATCH 03/24] feat(frontend): optional index canister --- .../icp-eth/services/custom-token.services.ts | 6 +- src/frontend/src/icp/api/icrc-ledger.api.ts | 19 ++++++ src/frontend/src/icp/derived/icrc.derived.ts | 2 +- .../services/ic-add-custom-tokens.service.ts | 60 ++++++++++++------- .../src/icp/validation/ic-token.validation.ts | 2 +- src/frontend/src/lib/i18n/en.json | 1 + src/frontend/src/lib/types/i18n.d.ts | 1 + 7 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/frontend/src/icp-eth/services/custom-token.services.ts b/src/frontend/src/icp-eth/services/custom-token.services.ts index 8000855898..54fac35c6d 100644 --- a/src/frontend/src/icp-eth/services/custom-token.services.ts +++ b/src/frontend/src/icp-eth/services/custom-token.services.ts @@ -10,7 +10,7 @@ import type { OptionIdentity } from '$lib/types/identity'; import type { Token } from '$lib/types/token'; import type { Identity } from '@dfinity/agent'; import { Principal } from '@dfinity/principal'; -import { isNullish, toNullable } from '@dfinity/utils'; +import { isNullish, nonNullish, toNullable } from '@dfinity/utils'; import { get } from 'svelte/store'; const assertErc20SendTokenData = (sendToken: Erc20Token): AutoLoadTokenResult | undefined => { @@ -72,7 +72,9 @@ export const toCustomToken = ({ token: { Icrc: { ledger_id: Principal.fromText(ledgerCanisterId), - index_id: toNullable(Principal.fromText(indexCanisterId)) + index_id: toNullable( + nonNullish(indexCanisterId) ? Principal.fromText(indexCanisterId) : undefined + ) } } }); diff --git a/src/frontend/src/icp/api/icrc-ledger.api.ts b/src/frontend/src/icp/api/icrc-ledger.api.ts index 97ffd74ba8..1766c49ce8 100644 --- a/src/frontend/src/icp/api/icrc-ledger.api.ts +++ b/src/frontend/src/icp/api/icrc-ledger.api.ts @@ -1,4 +1,5 @@ import { nowInBigIntNanoSeconds } from '$icp/utils/date.utils'; +import { getIcrcAccount } from '$icp/utils/icrc-account.utils'; import { getAgent } from '$lib/actors/agents.ic'; import type { CanisterIdText } from '$lib/types/canister'; import type { OptionIdentity } from '$lib/types/identity'; @@ -10,6 +11,7 @@ import { type IcrcSubaccount, type IcrcTokenMetadataResponse } from '@dfinity/ledger-icrc'; +import type { Tokens } from '@dfinity/ledger-icrc/dist/candid/icrc_ledger'; import { Principal } from '@dfinity/principal'; import { assertNonNullish, toNullable, type QueryParams } from '@dfinity/utils'; @@ -28,6 +30,23 @@ export const metadata = async ({ return metadata({ certified }); }; +export const balance = async ({ + certified = true, + owner, + identity, + ...rest +}: { + owner: Principal; + identity: OptionIdentity; + ledgerCanisterId: CanisterIdText; +} & QueryParams): Promise => { + assertNonNullish(identity); + + const { balance } = await ledgerCanister({ identity, ...rest }); + + return balance({ certified, ...getIcrcAccount(owner) }); +}; + export const transfer = async ({ identity, to, diff --git a/src/frontend/src/icp/derived/icrc.derived.ts b/src/frontend/src/icp/derived/icrc.derived.ts index 335e91cb79..69728f4a29 100644 --- a/src/frontend/src/icp/derived/icrc.derived.ts +++ b/src/frontend/src/icp/derived/icrc.derived.ts @@ -71,7 +71,7 @@ const icrcDefaultTokensToggleable: Readable = derived( userLedgerCanisterId === ledgerCanisterId && userIndexCanisterId === indexCanisterId ); - return mapDefaultTokenToToggleable({ + return mapDefaultTokenToToggleable({ defaultToken: { ledgerCanisterId, indexCanisterId, diff --git a/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts b/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts index d8da341e43..dc5bb32bee 100644 --- a/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts +++ b/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts @@ -1,5 +1,5 @@ import { getLedgerId, getTransactions as getTransactionsIcrc } from '$icp/api/icrc-index-ng.api'; -import { metadata } from '$icp/api/icrc-ledger.api'; +import { balance, metadata } from '$icp/api/icrc-ledger.api'; import type { IcCanisters, IcToken, IcTokenWithoutId } from '$icp/types/ic'; import { mapIcrcToken } from '$icp/utils/icrc.utils'; import { i18n } from '$lib/stores/i18n.store'; @@ -35,13 +35,6 @@ export const loadAndAssertAddCustomToken = async ({ return { result: 'error' }; } - if (isNullish(indexCanisterId)) { - toastsError({ - msg: { text: get(i18n).tokens.import.error.missing_index_id } - }); - return { result: 'error' }; - } - const canisterIds = { ledgerCanisterId, indexCanisterId }; const { alreadyAvailable } = assertAlreadyAvailable({ @@ -53,22 +46,26 @@ export const loadAndAssertAddCustomToken = async ({ return { result: 'error' }; } - const { valid } = await assertLedgerId({ - identity, - ...canisterIds - }); + const { valid } = isNullish(indexCanisterId) + ? { valid: true } + : await assertIndexLedgerId({ + identity, + ...canisterIds, + indexCanisterId + }); if (!valid) { return { result: 'error' }; } try { + const params = { identity, ...canisterIds }; + const [token, balance] = await Promise.all([ - loadMetadata({ - identity, - ...canisterIds - }), - loadBalance({ identity, ...canisterIds }) + loadMetadata(params), + ...(isNullish(indexCanisterId) + ? [loadLedgerBalance(params)] + : [loadIndexBalance({ ...params, indexCanisterId })]) ]); if (isNullish(token)) { @@ -157,10 +154,31 @@ const loadMetadata = async ({ } }; -const loadBalance = async ({ +const loadLedgerBalance = async ({ + identity, + ledgerCanisterId +}: IcCanisters & { identity: Identity }): Promise => { + try { + return await balance({ + ledgerCanisterId, + identity, + owner: identity.getPrincipal(), + certified: true + }); + } catch (err: unknown) { + toastsError({ + msg: { text: get(i18n).tokens.import.error.unexpected_ledger }, + err + }); + + throw err; + } +}; + +const loadIndexBalance = async ({ identity, indexCanisterId -}: Pick & { identity: Identity }): Promise => { +}: Required> & { identity: Identity }): Promise => { try { const { balance } = await getTransactionsIcrc({ indexCanisterId, @@ -181,11 +199,11 @@ const loadBalance = async ({ } }; -const assertLedgerId = async ({ +const assertIndexLedgerId = async ({ identity, indexCanisterId, ledgerCanisterId -}: IcCanisters & { identity: Identity }): Promise<{ valid: boolean }> => { +}: Required & { identity: Identity }): Promise<{ valid: boolean }> => { try { const ledgerId = await getLedgerId({ indexCanisterId, diff --git a/src/frontend/src/icp/validation/ic-token.validation.ts b/src/frontend/src/icp/validation/ic-token.validation.ts index 43a38ce3f3..3c86af397c 100644 --- a/src/frontend/src/icp/validation/ic-token.validation.ts +++ b/src/frontend/src/icp/validation/ic-token.validation.ts @@ -16,7 +16,7 @@ export const IcAppMetadataSchema = z.object({ export const IcCanistersSchema = z.object({ ledgerCanisterId: CanisterIdTextSchema, - indexCanisterId: CanisterIdTextSchema + indexCanisterId: CanisterIdTextSchema.optional() }); export const IcCkLinkedAssetsSchema = z.object({ diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index a8c8dc29f0..f3d89ef332 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -435,6 +435,7 @@ "error": { "loading_metadata": "Error while loading the metadata of the token.", "no_metadata": "No metadata for the token is provided. This is unexpected.", + "unexpected_ledger": "Something went wrong while validating the Ledger canister.", "unexpected_index": "Something went wrong while validating the Index canister.", "unexpected_index_ledger": "Something went wrong while loading the Ledger ID related to the Index canister.", "invalid_ledger_id": "The Ledger ID is not related to the Index canister.", diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 01e4743a3e..802356e3b1 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -396,6 +396,7 @@ interface I18nTokens { error: { loading_metadata: string; no_metadata: string; + unexpected_ledger: string; unexpected_index: string; unexpected_index_ledger: string; invalid_ledger_id: string; From 461dcfd5ce69bec5b3747401ac96bf31302e1997 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 09:55:04 +0100 Subject: [PATCH 04/24] feat: use principal schema --- src/frontend/src/icp/validation/ic-token.validation.ts | 2 +- src/frontend/src/lib/types/canister.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/icp/validation/ic-token.validation.ts b/src/frontend/src/icp/validation/ic-token.validation.ts index 43a38ce3f3..48e8af65ae 100644 --- a/src/frontend/src/icp/validation/ic-token.validation.ts +++ b/src/frontend/src/icp/validation/ic-token.validation.ts @@ -1,6 +1,6 @@ import { CanisterIdTextSchema } from '$lib/types/canister'; -import { CoingeckoCoinsIdSchema } from '$lib/types/coingecko'; import { TokenSchema } from '$lib/types/token'; +import { CoingeckoCoinsIdSchema } from '$lib/validation/coingecko.validation'; import { UrlSchema } from '$lib/validation/url.validation'; import { z } from 'zod'; diff --git a/src/frontend/src/lib/types/canister.ts b/src/frontend/src/lib/types/canister.ts index 51f8b3678a..1a5d8564ba 100644 --- a/src/frontend/src/lib/types/canister.ts +++ b/src/frontend/src/lib/types/canister.ts @@ -1,11 +1,12 @@ import type { OptionIdentity } from '$lib/types/identity'; import type { Option } from '$lib/types/utils'; +import { PrincipalTextSchema } from '$lib/validation/principal.validation'; import type { Identity } from '@dfinity/agent'; import { Principal } from '@dfinity/principal'; import type { CanisterOptions } from '@dfinity/utils'; import { z } from 'zod'; -export const CanisterIdTextSchema = z.string(); +export const CanisterIdTextSchema = PrincipalTextSchema; export type CanisterIdText = z.infer; From 3359445476ebefd109ff0d04da667ef044796231 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 09:57:44 +0100 Subject: [PATCH 05/24] test: proper canister id --- .../src/tests/icp/validation/ic-token.validation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts index 4fdb32f3a5..c4d51274b4 100644 --- a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -231,7 +231,7 @@ describe('Schema Validation Tests', () => { it('should fail with invalid canister', () => { const invalidData = { ...validData, - feeLedgerCanisterId: 123 + ledgerCanisterId: 123 }; expect(() => IcTokenSchema.parse(invalidData)).toThrow(); }); From 86a35cdcb33a220546a76e0e660693b5bbc74a42 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 13:02:15 +0100 Subject: [PATCH 06/24] feat: IcCanistersStrictSchema --- .../src/icp/validation/ic-token.validation.ts | 4 ++++ .../icp/validation/ic-token.validation.spec.ts | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/icp/validation/ic-token.validation.ts b/src/frontend/src/icp/validation/ic-token.validation.ts index 78dd954f2d..33222f0b53 100644 --- a/src/frontend/src/icp/validation/ic-token.validation.ts +++ b/src/frontend/src/icp/validation/ic-token.validation.ts @@ -19,6 +19,10 @@ export const IcCanistersSchema = z.object({ indexCanisterId: CanisterIdTextSchema.optional() }); +export const IcCanistersStrictSchema = IcCanistersSchema.extend({ + indexCanisterId: CanisterIdTextSchema +}); + export const IcCkLinkedAssetsSchema = z.object({ twinToken: TokenSchema, feeLedgerCanisterId: CanisterIdTextSchema.optional() diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts index c4d51274b4..4f8a94a566 100644 --- a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -7,6 +7,7 @@ import { import { IcAppMetadataSchema, IcCanistersSchema, + IcCanistersStrictSchema, IcCkInterfaceSchema, IcCkLinkedAssetsSchema, IcCkMetadataSchema, @@ -98,6 +99,13 @@ describe('Schema Validation Tests', () => { expect(IcCanistersSchema.parse(validData)).toEqual(validData); }); + it('should validate with ledger canister only', () => { + const validData = { + ledgerCanisterId: mockCanisters.ledgerCanisterId + }; + expect(IcCanistersSchema.parse(validData)).toEqual(validData); + }); + it('should fail with invalid ledger canister id', () => { const invalidData = { ...validData, @@ -120,12 +128,14 @@ describe('Schema Validation Tests', () => { }; expect(() => IcCanistersSchema.parse(invalidData)).toThrow(); }); + }); + describe('IcCanistersStrictSchema', () => { it('should fail with missing index canister field', () => { const invalidData = { ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID }; - expect(() => IcCanistersSchema.parse(invalidData)).toThrow(); + expect(() => IcCanistersStrictSchema.parse(invalidData)).toThrow(); }); }); @@ -183,6 +193,11 @@ describe('Schema Validation Tests', () => { expect(IcInterfaceSchema.parse(validData)).toEqual(validData); }); + it('should validate without Index canister', () => { + const { indexCanisterId: _, ...restValidData } = validData; + expect(IcInterfaceSchema.parse(restValidData)).toEqual(restValidData); + }); + it('should fail with incorrect IcCanisters data', () => { const invalidData = { ...validData, From f13e646a1df4b1e155122e7df9d34a95c9da2f74 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 13:35:25 +0100 Subject: [PATCH 07/24] feat: assert index canister id --- .../transactions/IcTransactions.svelte | 7 +++++++ .../src/icp/services/ic-transactions.services.ts | 6 +++--- src/frontend/src/icp/types/ic-token.ts | 4 +++- src/frontend/src/icp/utils/ic-token.utils.ts | 13 +++++++++++++ .../icp/validation/ic-token.validation.spec.ts | 16 ++++++++++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/icp/utils/ic-token.utils.ts diff --git a/src/frontend/src/icp/components/transactions/IcTransactions.svelte b/src/frontend/src/icp/components/transactions/IcTransactions.svelte index e8a68bc21c..f600ced922 100644 --- a/src/frontend/src/icp/components/transactions/IcTransactions.svelte +++ b/src/frontend/src/icp/components/transactions/IcTransactions.svelte @@ -32,6 +32,7 @@ import { modalStore } from '$lib/stores/modal.store'; import { token } from '$lib/stores/token.store'; import { last } from '$lib/utils/array.utils'; + import { isIcToken, isIcTokenCanistersStrict } from '$icp/utils/ic-token.utils'; let ckEthereum: boolean; $: ckEthereum = $tokenCkEthLedger || $tokenCkErc20Ledger; @@ -69,6 +70,12 @@ return; } + if (!isIcToken($tokenAsIcToken) || !isIcTokenCanistersStrict($tokenAsIcToken)) { + // On one hand, we assume that the parent component does not mount this component if no transactions can be fetched; on the other hand, we want to avoid displaying an error toast that could potentially appear multiple times. + // Therefore, we do not particularly display a visual error. In any case, we cannot load transactions without an Index canister. + return; + } + await loadNextTransactions({ owner: $authIdentity.getPrincipal(), identity: $authIdentity, diff --git a/src/frontend/src/icp/services/ic-transactions.services.ts b/src/frontend/src/icp/services/ic-transactions.services.ts index 09bd1c945f..88ded76762 100644 --- a/src/frontend/src/icp/services/ic-transactions.services.ts +++ b/src/frontend/src/icp/services/ic-transactions.services.ts @@ -1,7 +1,7 @@ import { getTransactions as getTransactionsIcp } from '$icp/api/icp-index.api'; import { getTransactions as getTransactionsIcrc } from '$icp/api/icrc-index-ng.api'; import { icTransactionsStore } from '$icp/stores/ic-transactions.store'; -import type { IcToken } from '$icp/types/ic-token'; +import type { IcCanistersStrict, IcToken } from '$icp/types/ic-token'; import type { IcTransaction } from '$icp/types/ic-transaction'; import { mapIcTransaction } from '$icp/utils/ic-transactions.utils'; import { mapTransactionIcpToSelf } from '$icp/utils/icp-transactions.utils'; @@ -23,7 +23,7 @@ const getTransactions = async ({ identity: OptionIdentity; start?: bigint; maxResults?: bigint; - token: IcToken; + token: IcToken & IcCanistersStrict; }): Promise => { if (standard === 'icrc') { const { transactions } = await getTransactionsIcrc({ @@ -49,7 +49,7 @@ export const loadNextTransactions = ({ identity: OptionIdentity; start?: bigint; maxResults?: bigint; - token: IcToken; + token: IcToken & IcCanistersStrict; signalEnd: () => void; }): Promise => queryAndUpdate({ diff --git a/src/frontend/src/icp/types/ic-token.ts b/src/frontend/src/icp/types/ic-token.ts index ef285fde50..1308ec82b3 100644 --- a/src/frontend/src/icp/types/ic-token.ts +++ b/src/frontend/src/icp/types/ic-token.ts @@ -1,6 +1,6 @@ import { IcAppMetadataSchema, - IcCanistersSchema, + IcCanistersSchema, IcCanistersStrictSchema, IcCkInterfaceSchema, IcCkLinkedAssetsSchema, IcCkMetadataSchema, @@ -19,6 +19,8 @@ export type IcAppMetadata = z.infer; export type IcCanisters = z.infer; +export type IcCanistersStrict = z.infer; + export type IcCkLinkedAssets = z.infer; export type IcCkMetadata = z.infer; diff --git a/src/frontend/src/icp/utils/ic-token.utils.ts b/src/frontend/src/icp/utils/ic-token.utils.ts new file mode 100644 index 0000000000..72f136a4a1 --- /dev/null +++ b/src/frontend/src/icp/utils/ic-token.utils.ts @@ -0,0 +1,13 @@ +import type { IcCanistersStrict, IcToken } from '$icp/types/ic-token'; +import { IcCanistersStrictSchema, IcTokenSchema } from '$icp/validation/ic-token.validation'; +import type { Token } from '$lib/types/token'; + +export const isIcToken = (token: Token): token is IcToken => { + const { success } = IcTokenSchema.safeParse(token); + return success; +}; + +export const isIcTokenCanistersStrict = (token: IcToken): token is IcToken & IcCanistersStrict => { + const { success } = IcCanistersStrictSchema.safeParse(token); + return success; +}; diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts index 4f8a94a566..d2bd2bd249 100644 --- a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -131,12 +131,28 @@ describe('Schema Validation Tests', () => { }); describe('IcCanistersStrictSchema', () => { + const validToken = { + ...mockToken, + ...mockFee, + ...mockCanisters, + ...mockApp + }; + + it('should validate with correct data', () => { + expect(IcCanistersSchema.parse(validToken)).toEqual(validToken); + }); + it('should fail with missing index canister field', () => { const invalidData = { ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID }; expect(() => IcCanistersStrictSchema.parse(invalidData)).toThrow(); }); + + it('should fail for token with missing index canister field', () => { + const { indexCanisterId: _, ...tokenWithoutIndexCanisterId } = validToken; + expect(() => IcCanistersStrictSchema.parse(tokenWithoutIndexCanisterId)).toThrow(); + }); }); describe('IcCkLinkedAssetsSchema', () => { From 2b5cb48c6b2a0ec66849cb3466701ec0ccaaf25c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:36:54 +0000 Subject: [PATCH 08/24] =?UTF-8?q?=F0=9F=A4=96=20Apply=20formatting=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/icp/components/transactions/IcTransactions.svelte | 2 +- src/frontend/src/icp/types/ic-token.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/icp/components/transactions/IcTransactions.svelte b/src/frontend/src/icp/components/transactions/IcTransactions.svelte index f600ced922..295a28509a 100644 --- a/src/frontend/src/icp/components/transactions/IcTransactions.svelte +++ b/src/frontend/src/icp/components/transactions/IcTransactions.svelte @@ -22,6 +22,7 @@ import { icTransactions } from '$icp/derived/ic-transactions.derived'; import { loadNextTransactions } from '$icp/services/ic-transactions.services'; import type { IcTransactionUi } from '$icp/types/ic-transaction'; + import { isIcToken, isIcTokenCanistersStrict } from '$icp/utils/ic-token.utils'; import TransactionsPlaceholder from '$lib/components/transactions/TransactionsPlaceholder.svelte'; import Header from '$lib/components/ui/Header.svelte'; import { WALLET_PAGINATION } from '$lib/constants/app.constants'; @@ -32,7 +33,6 @@ import { modalStore } from '$lib/stores/modal.store'; import { token } from '$lib/stores/token.store'; import { last } from '$lib/utils/array.utils'; - import { isIcToken, isIcTokenCanistersStrict } from '$icp/utils/ic-token.utils'; let ckEthereum: boolean; $: ckEthereum = $tokenCkEthLedger || $tokenCkErc20Ledger; diff --git a/src/frontend/src/icp/types/ic-token.ts b/src/frontend/src/icp/types/ic-token.ts index 1308ec82b3..d339a2b4ed 100644 --- a/src/frontend/src/icp/types/ic-token.ts +++ b/src/frontend/src/icp/types/ic-token.ts @@ -1,6 +1,7 @@ import { IcAppMetadataSchema, - IcCanistersSchema, IcCanistersStrictSchema, + IcCanistersSchema, + IcCanistersStrictSchema, IcCkInterfaceSchema, IcCkLinkedAssetsSchema, IcCkMetadataSchema, From e44d2a03b07d865a521c27f8aa751b46b5075959 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 13:38:24 +0100 Subject: [PATCH 09/24] feat: isNot --- .../icp/components/transactions/IcTransactions.svelte | 9 +++++++-- src/frontend/src/icp/utils/ic-token.utils.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/icp/components/transactions/IcTransactions.svelte b/src/frontend/src/icp/components/transactions/IcTransactions.svelte index f600ced922..a570b3a848 100644 --- a/src/frontend/src/icp/components/transactions/IcTransactions.svelte +++ b/src/frontend/src/icp/components/transactions/IcTransactions.svelte @@ -32,7 +32,12 @@ import { modalStore } from '$lib/stores/modal.store'; import { token } from '$lib/stores/token.store'; import { last } from '$lib/utils/array.utils'; - import { isIcToken, isIcTokenCanistersStrict } from '$icp/utils/ic-token.utils'; + import { + isIcToken, + isIcTokenCanistersStrict, + isNotIcToken, + isNotIcTokenCanistersStrict + } from '$icp/utils/ic-token.utils'; let ckEthereum: boolean; $: ckEthereum = $tokenCkEthLedger || $tokenCkErc20Ledger; @@ -70,7 +75,7 @@ return; } - if (!isIcToken($tokenAsIcToken) || !isIcTokenCanistersStrict($tokenAsIcToken)) { + if (isNotIcToken($tokenAsIcToken) || isNotIcTokenCanistersStrict($tokenAsIcToken)) { // On one hand, we assume that the parent component does not mount this component if no transactions can be fetched; on the other hand, we want to avoid displaying an error toast that could potentially appear multiple times. // Therefore, we do not particularly display a visual error. In any case, we cannot load transactions without an Index canister. return; diff --git a/src/frontend/src/icp/utils/ic-token.utils.ts b/src/frontend/src/icp/utils/ic-token.utils.ts index 72f136a4a1..86df48f6ef 100644 --- a/src/frontend/src/icp/utils/ic-token.utils.ts +++ b/src/frontend/src/icp/utils/ic-token.utils.ts @@ -7,7 +7,17 @@ export const isIcToken = (token: Token): token is IcToken => { return success; }; +export const isNotIcToken = (token: Token): token is Exclude => { + return !isIcToken(token); +}; + export const isIcTokenCanistersStrict = (token: IcToken): token is IcToken & IcCanistersStrict => { const { success } = IcCanistersStrictSchema.safeParse(token); return success; }; + +export const isNotIcTokenCanistersStrict = ( + token: IcToken +): token is Exclude => { + return !isIcTokenCanistersStrict(token); +}; From 105837021a7a50b0b211c19487dac05d0bcf6d2c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 13:39:02 +0100 Subject: [PATCH 10/24] feat: isNot --- .../icp/components/transactions/IcTransactions.svelte | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/icp/components/transactions/IcTransactions.svelte b/src/frontend/src/icp/components/transactions/IcTransactions.svelte index 295a28509a..d12353278c 100644 --- a/src/frontend/src/icp/components/transactions/IcTransactions.svelte +++ b/src/frontend/src/icp/components/transactions/IcTransactions.svelte @@ -22,7 +22,12 @@ import { icTransactions } from '$icp/derived/ic-transactions.derived'; import { loadNextTransactions } from '$icp/services/ic-transactions.services'; import type { IcTransactionUi } from '$icp/types/ic-transaction'; - import { isIcToken, isIcTokenCanistersStrict } from '$icp/utils/ic-token.utils'; + import { + isIcToken, + isIcTokenCanistersStrict, + isNotIcToken, + isNotIcTokenCanistersStrict + } from '$icp/utils/ic-token.utils'; import TransactionsPlaceholder from '$lib/components/transactions/TransactionsPlaceholder.svelte'; import Header from '$lib/components/ui/Header.svelte'; import { WALLET_PAGINATION } from '$lib/constants/app.constants'; @@ -70,7 +75,7 @@ return; } - if (!isIcToken($tokenAsIcToken) || !isIcTokenCanistersStrict($tokenAsIcToken)) { + if (isNotIcToken($tokenAsIcToken) || isNotIcTokenCanistersStrict($tokenAsIcToken)) { // On one hand, we assume that the parent component does not mount this component if no transactions can be fetched; on the other hand, we want to avoid displaying an error toast that could potentially appear multiple times. // Therefore, we do not particularly display a visual error. In any case, we cannot load transactions without an Index canister. return; From f33f911285b34646cd28789024f9c80669b479a6 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 13:43:31 +0100 Subject: [PATCH 11/24] feat: tmp workaround --- src/frontend/src/icp/workers/icrc-wallet.worker.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/icp/workers/icrc-wallet.worker.ts b/src/frontend/src/icp/workers/icrc-wallet.worker.ts index 02807464ea..c0cf1366b7 100644 --- a/src/frontend/src/icp/workers/icrc-wallet.worker.ts +++ b/src/frontend/src/icp/workers/icrc-wallet.worker.ts @@ -25,13 +25,18 @@ const getTransactions = ({ }: SchedulerJobParams): Promise => { assertNonNullish(data, 'No data - indexCanisterId - provided to fetch transactions.'); + // TODO: This is not clean. If the index canister ID is not provided we should not even land here. + const { indexCanisterId } = data; + assertNonNullish(indexCanisterId); + return getTransactionsApi({ identity, certified, owner: identity.getPrincipal(), // We query tip to discover the new transactions start: undefined, - ...data + ...data, + indexCanisterId }); }; From 3c6e04f2059bbc44ae0ef85ecc6124f61e8526c3 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 13:45:53 +0100 Subject: [PATCH 12/24] test: validation index --- .../tests/icp/validation/ic-token.validation.spec.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts index d2bd2bd249..35f303b321 100644 --- a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -139,7 +139,16 @@ describe('Schema Validation Tests', () => { }; it('should validate with correct data', () => { - expect(IcCanistersSchema.parse(validToken)).toEqual(validToken); + const validData = { + ledgerCanisterId: mockCanisters.ledgerCanisterId, + indexCanisterId: mockCanisters.ledgerCanisterId + }; + + expect(IcCanistersSchema.parse(validData)).toEqual(validData); + }); + + it('should validate a token with index canister correct data', () => { + expect(() => IcCanistersStrictSchema.parse(validToken)).not.toThrow(); }); it('should fail with missing index canister field', () => { From a2eaed5e4f66feec44059a1463fba288b20ed19e Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 13:49:37 +0100 Subject: [PATCH 13/24] chore: lint --- .../src/icp/components/transactions/IcTransactions.svelte | 7 +------ src/frontend/src/icp/utils/ic-token.utils.ts | 8 ++------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/frontend/src/icp/components/transactions/IcTransactions.svelte b/src/frontend/src/icp/components/transactions/IcTransactions.svelte index d12353278c..d3d2dcd483 100644 --- a/src/frontend/src/icp/components/transactions/IcTransactions.svelte +++ b/src/frontend/src/icp/components/transactions/IcTransactions.svelte @@ -22,12 +22,7 @@ import { icTransactions } from '$icp/derived/ic-transactions.derived'; import { loadNextTransactions } from '$icp/services/ic-transactions.services'; import type { IcTransactionUi } from '$icp/types/ic-transaction'; - import { - isIcToken, - isIcTokenCanistersStrict, - isNotIcToken, - isNotIcTokenCanistersStrict - } from '$icp/utils/ic-token.utils'; + import { isNotIcToken, isNotIcTokenCanistersStrict } from '$icp/utils/ic-token.utils'; import TransactionsPlaceholder from '$lib/components/transactions/TransactionsPlaceholder.svelte'; import Header from '$lib/components/ui/Header.svelte'; import { WALLET_PAGINATION } from '$lib/constants/app.constants'; diff --git a/src/frontend/src/icp/utils/ic-token.utils.ts b/src/frontend/src/icp/utils/ic-token.utils.ts index 86df48f6ef..e10206dc2d 100644 --- a/src/frontend/src/icp/utils/ic-token.utils.ts +++ b/src/frontend/src/icp/utils/ic-token.utils.ts @@ -7,9 +7,7 @@ export const isIcToken = (token: Token): token is IcToken => { return success; }; -export const isNotIcToken = (token: Token): token is Exclude => { - return !isIcToken(token); -}; +export const isNotIcToken = (token: Token): token is Exclude => !isIcToken(token); export const isIcTokenCanistersStrict = (token: IcToken): token is IcToken & IcCanistersStrict => { const { success } = IcCanistersStrictSchema.safeParse(token); @@ -18,6 +16,4 @@ export const isIcTokenCanistersStrict = (token: IcToken): token is IcToken & IcC export const isNotIcTokenCanistersStrict = ( token: IcToken -): token is Exclude => { - return !isIcTokenCanistersStrict(token); -}; +): token is Exclude => !isIcTokenCanistersStrict(token); From 9b2ba90762165d0ab91d0af36499d7ecd9beed6a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 16:49:34 +0100 Subject: [PATCH 14/24] feat: redo after merge --- .../src/icp/schema/ic-token.schema.ts | 3 +-- src/frontend/src/icp/utils/ic-token.utils.ts | 19 ------------- .../tests/icp/schema/ic-token.schema.spec.ts | 22 +++++++-------- .../validation/ic-token.validation.spec.ts | 27 ++++++++++--------- 4 files changed, 25 insertions(+), 46 deletions(-) delete mode 100644 src/frontend/src/icp/utils/ic-token.utils.ts diff --git a/src/frontend/src/icp/schema/ic-token.schema.ts b/src/frontend/src/icp/schema/ic-token.schema.ts index 9630ee0dd5..58bcf682e2 100644 --- a/src/frontend/src/icp/schema/ic-token.schema.ts +++ b/src/frontend/src/icp/schema/ic-token.schema.ts @@ -16,8 +16,7 @@ export const IcAppMetadataSchema = z.object({ export const IcCanistersSchema = z.object({ ledgerCanisterId: CanisterIdTextSchema, - // TODO: Make canister .optional() - indexCanisterId: CanisterIdTextSchema + indexCanisterId: CanisterIdTextSchema.optional() }); export const IcCanistersStrictSchema = IcCanistersSchema.extend({ diff --git a/src/frontend/src/icp/utils/ic-token.utils.ts b/src/frontend/src/icp/utils/ic-token.utils.ts deleted file mode 100644 index e10206dc2d..0000000000 --- a/src/frontend/src/icp/utils/ic-token.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { IcCanistersStrict, IcToken } from '$icp/types/ic-token'; -import { IcCanistersStrictSchema, IcTokenSchema } from '$icp/validation/ic-token.validation'; -import type { Token } from '$lib/types/token'; - -export const isIcToken = (token: Token): token is IcToken => { - const { success } = IcTokenSchema.safeParse(token); - return success; -}; - -export const isNotIcToken = (token: Token): token is Exclude => !isIcToken(token); - -export const isIcTokenCanistersStrict = (token: IcToken): token is IcToken & IcCanistersStrict => { - const { success } = IcCanistersStrictSchema.safeParse(token); - return success; -}; - -export const isNotIcTokenCanistersStrict = ( - token: IcToken -): token is Exclude => !isIcTokenCanistersStrict(token); diff --git a/src/frontend/src/tests/icp/schema/ic-token.schema.spec.ts b/src/frontend/src/tests/icp/schema/ic-token.schema.spec.ts index 992204bf2a..a426ef89b1 100644 --- a/src/frontend/src/tests/icp/schema/ic-token.schema.spec.ts +++ b/src/frontend/src/tests/icp/schema/ic-token.schema.spec.ts @@ -99,13 +99,12 @@ describe('ic-token.schema', () => { expect(IcCanistersSchema.parse(validData)).toEqual(validData); }); - // TODO: uncomment when Index canister becomes optional - // it('should validate with ledger canister only', () => { - // const validData = { - // ledgerCanisterId: mockCanisters.ledgerCanisterId - // }; - // expect(IcCanistersSchema.parse(validData)).toEqual(validData); - // }); + it('should validate with ledger canister only', () => { + const validData = { + ledgerCanisterId: mockCanisters.ledgerCanisterId + }; + expect(IcCanistersSchema.parse(validData)).toEqual(validData); + }); it('should fail with invalid ledger canister id', () => { const invalidData = { @@ -219,11 +218,10 @@ describe('ic-token.schema', () => { expect(IcInterfaceSchema.parse(validData)).toEqual(validData); }); - // TODO: uncomment when Index canister becomes optional - // it('should validate without Index canister', () => { - // const { indexCanisterId: _, ...restValidData } = validData; - // expect(IcInterfaceSchema.parse(restValidData)).toEqual(restValidData); - // }); + it('should validate without Index canister', () => { + const { indexCanisterId: _, ...restValidData } = validData; + expect(IcInterfaceSchema.parse(restValidData)).toEqual(restValidData); + }); it('should fail with incorrect IcCanisters data', () => { const invalidData = { diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts index d18122cab7..42dfa81889 100644 --- a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -23,9 +23,7 @@ describe('ic-token.validation', () => { }; const validIcCanisters: IcCanisters = { - ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID, - // TODO: to be removed when indexCanisterId becomes optional - indexCanisterId: IC_CKBTC_INDEX_CANISTER_ID + ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID }; const validIcToken: IcToken = { @@ -35,6 +33,11 @@ describe('ic-token.validation', () => { position: 1 }; + const validIcTokenWithIndex: IcToken = { + ...validIcToken, + indexCanisterId: IC_CKBTC_INDEX_CANISTER_ID + }; + describe('isIcToken', () => { it('should return true for a valid IcToken', () => { expect(isIcToken(validIcToken)).toBe(true); @@ -57,13 +60,12 @@ describe('ic-token.validation', () => { describe('isIcTokenCanistersStrict', () => { it('should return true for a valid IcToken with IcCanistersStrict', () => { - expect(isIcTokenCanistersStrict(validIcToken)).toBe(true); + expect(isIcTokenCanistersStrict(validIcTokenWithIndex)).toBe(true); }); - // TODO: test missing indexCanisterId when it becomes optional - // it('should return false for a valid IcToken without strict canisters fields', () => { - // expect(isIcTokenCanistersStrict(validIcToken)).toBe(false); - // }); + it('should return false for a valid IcToken without strict canisters fields', () => { + expect(isIcTokenCanistersStrict(validIcToken)).toBe(false); + }); it('should return false for a token type casted to IcToken', () => { expect(isIcTokenCanistersStrict(validToken as IcToken)).toBe(false); @@ -72,13 +74,12 @@ describe('ic-token.validation', () => { describe('isNotIcTokenCanistersStrict', () => { it('should return false for a valid IcToken with IcCanistersStrict', () => { - expect(isNotIcTokenCanistersStrict(validIcToken)).toBe(false); + expect(isNotIcTokenCanistersStrict(validIcTokenWithIndex)).toBe(false); }); - // TODO: test missing indexCanisterId when it becomes optional - // it('should return true for a valid IcToken without strict canisters fields', () => { - // expect(isNotIcTokenCanistersStrict(validIcToken)).toBe(true); - // }); + it('should return true for a valid IcToken without strict canisters fields', () => { + expect(isNotIcTokenCanistersStrict(validIcToken)).toBe(true); + }); it('should return true for a token type casted to IcToken', () => { expect(isNotIcTokenCanistersStrict(validToken as IcToken)).toBe(true); From 1ff7ce7aa6d47afe577fbeed0d1281b3cc3820e0 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Nov 2024 16:50:52 +0100 Subject: [PATCH 15/24] feat: redo after merge --- .../src/icp/components/transactions/IcTransactions.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/icp/components/transactions/IcTransactions.svelte b/src/frontend/src/icp/components/transactions/IcTransactions.svelte index d3d2dcd483..62cb8a038e 100644 --- a/src/frontend/src/icp/components/transactions/IcTransactions.svelte +++ b/src/frontend/src/icp/components/transactions/IcTransactions.svelte @@ -22,7 +22,7 @@ import { icTransactions } from '$icp/derived/ic-transactions.derived'; import { loadNextTransactions } from '$icp/services/ic-transactions.services'; import type { IcTransactionUi } from '$icp/types/ic-transaction'; - import { isNotIcToken, isNotIcTokenCanistersStrict } from '$icp/utils/ic-token.utils'; + import { isNotIcToken, isNotIcTokenCanistersStrict } from '$icp/validation/ic-token.validation'; import TransactionsPlaceholder from '$lib/components/transactions/TransactionsPlaceholder.svelte'; import Header from '$lib/components/ui/Header.svelte'; import { WALLET_PAGINATION } from '$lib/constants/app.constants'; From 88a22e63a1c40c5f9ffc9710ffad38cdbc234822 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 6 Nov 2024 09:51:20 +0100 Subject: [PATCH 16/24] test: can call without index canister --- .../ic-add-custom-tokens.service.spec.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts b/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts index b34bfb948f..81e42cff4e 100644 --- a/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts +++ b/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts @@ -97,20 +97,6 @@ describe('ic-add-custom-tokens.service', () => { }); }); - it('should return error if indexCanisterId is missing', async () => { - const result = await loadAndAssertAddCustomToken({ - identity: mockIdentity, - icrcTokens: [], - ledgerCanisterId: mockLedgerCanisterId - }); - - expect(result).toEqual({ result: 'error' }); - - expect(spyToastsError).toHaveBeenNthCalledWith(1, { - msg: { text: get(i18n).tokens.import.error.missing_index_id } - }); - }); - it('should return error if token is already available', async () => { const result = await loadAndAssertAddCustomToken({ identity: mockIdentity, @@ -223,6 +209,16 @@ describe('ic-add-custom-tokens.service', () => { ]); }); + it('should accept loading without indexCanisterId', async () => { + const { result } = await loadAndAssertAddCustomToken({ + identity: mockIdentity, + icrcTokens: [], + ledgerCanisterId: mockLedgerCanisterId + }); + + expect(result).toBe('success'); + }); + it('should init ledger with expected canister id', async () => { await loadAndAssertAddCustomToken(validParams); From 985427912eaa2a7516fd6ce87c0ea7c067a95e93 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 6 Nov 2024 10:13:57 +0100 Subject: [PATCH 17/24] test: without index canister --- .../ic-add-custom-tokens.service.spec.ts | 313 ++++++++++++------ 1 file changed, 208 insertions(+), 105 deletions(-) diff --git a/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts b/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts index 81e42cff4e..b15196a45d 100644 --- a/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts +++ b/src/frontend/src/tests/icp/services/ic-add-custom-tokens.service.spec.ts @@ -1,12 +1,13 @@ import { ICP_NETWORK } from '$env/networks.env'; import { loadAndAssertAddCustomToken } from '$icp/services/ic-add-custom-tokens.service'; -import type { IcToken } from '$icp/types/ic-token'; +import type { IcCanisters, IcToken } from '$icp/types/ic-token'; import { getIcrcAccount } from '$icp/utils/icrc-account.utils'; import * as agent from '$lib/actors/agents.ic'; import { i18n } from '$lib/stores/i18n.store'; import * as toastsStore from '$lib/stores/toasts.store'; +import type { OptionIdentity } from '$lib/types/identity'; import { parseTokenId } from '$lib/validation/token.validation'; -import { mockIdentity } from '$tests/mocks/identity.mock'; +import { mockIdentity, mockPrincipal } from '$tests/mocks/identity.mock'; import type { HttpAgent } from '@dfinity/agent'; import { IcrcIndexNgCanister, IcrcLedgerCanister } from '@dfinity/ledger-icrc'; import { Principal } from '@dfinity/principal'; @@ -30,12 +31,12 @@ describe('ic-add-custom-tokens.service', () => { let spyLedgerId: MockInstance; let spyGetTransactions: MockInstance; let spyMetadata: MockInstance; + let spyBalance: MockInstance; const validParams = { identity: mockIdentity, icrcTokens: [], - ledgerCanisterId: mockLedgerCanisterId, - indexCanisterId: mockIndexCanisterId + ledgerCanisterId: mockLedgerCanisterId }; const tokenName = 'Test Token'; @@ -126,81 +127,118 @@ describe('ic-add-custom-tokens.service', () => { }); }); - it('should return error if ledger is not related to index', async () => { - indexCanisterMock.ledgerId.mockResolvedValue( - Principal.fromText('2ouva-viaaa-aaaaq-aaamq-cai') - ); + describe('without index canister', () => { + it('should return error if metadata are undefined', async () => { + spyBalance = ledgerCanisterMock.balance.mockResolvedValue(123n); - const result = await loadAndAssertAddCustomToken(validParams); + spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([]); - expect(result).toEqual({ result: 'error' }); - }); + const result = await loadAndAssertAddCustomToken(validParams); - it('should return error if metadata are undefined', async () => { - spyLedgerId = indexCanisterMock.ledgerId.mockResolvedValue( - Principal.fromText(mockLedgerCanisterId) - ); + expect(result).toEqual({ result: 'error' }); - spyGetTransactions = indexCanisterMock.getTransactions.mockResolvedValue({ - balance: 100n, - transactions: [], - oldest_tx_id: [0n] + expect(spyToastsError).toHaveBeenNthCalledWith(1, { + msg: { text: get(i18n).tokens.import.error.no_metadata } + }); }); - spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([]); + it('should return error if token already exits', async () => { + spyBalance = ledgerCanisterMock.balance.mockResolvedValue(123n); - const result = await loadAndAssertAddCustomToken(validParams); + spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([ + ['icrc1:name', { Text: tokenName }], + ['icrc1:symbol', { Text: tokenSymbol }], + ['icrc1:decimals', { Nat: BigInt(tokenDecimals) }], + ['icrc1:fee', { Nat: tokenFee }] + ]); - expect(result).toEqual({ result: 'error' }); + const result = await loadAndAssertAddCustomToken({ + ...validParams, + icrcTokens: [existingToken] + }); - expect(spyToastsError).toHaveBeenNthCalledWith(1, { - msg: { text: get(i18n).tokens.import.error.no_metadata } + expect(result).toEqual({ result: 'error' }); + + expect(spyToastsError).toHaveBeenNthCalledWith(1, { + msg: { text: get(i18n).tokens.error.duplicate_metadata } + }); }); }); - it('should return error if token already exits', async () => { - spyLedgerId = indexCanisterMock.ledgerId.mockResolvedValue( - Principal.fromText(mockLedgerCanisterId) - ); + describe('with index canister', () => { + it('should return error if ledger is not related to index', async () => { + indexCanisterMock.ledgerId.mockResolvedValue( + Principal.fromText('2ouva-viaaa-aaaaq-aaamq-cai') + ); + + const result = await loadAndAssertAddCustomToken({ + ...validParams, + indexCanisterId: mockIndexCanisterId + }); - spyGetTransactions = indexCanisterMock.getTransactions.mockResolvedValue({ - balance: 100n, - transactions: [], - oldest_tx_id: [0n] + expect(result).toEqual({ result: 'error' }); }); - spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([ - ['icrc1:name', { Text: tokenName }], - ['icrc1:symbol', { Text: tokenSymbol }], - ['icrc1:decimals', { Nat: BigInt(tokenDecimals) }], - ['icrc1:fee', { Nat: tokenFee }] - ]); + it('should return error if metadata are undefined', async () => { + spyLedgerId = indexCanisterMock.ledgerId.mockResolvedValue( + Principal.fromText(mockLedgerCanisterId) + ); - const result = await loadAndAssertAddCustomToken({ - ...validParams, - icrcTokens: [existingToken] - }); + spyGetTransactions = indexCanisterMock.getTransactions.mockResolvedValue({ + balance: 100n, + transactions: [], + oldest_tx_id: [0n] + }); - expect(result).toEqual({ result: 'error' }); + spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([]); - expect(spyToastsError).toHaveBeenNthCalledWith(1, { - msg: { text: get(i18n).tokens.error.duplicate_metadata } + const result = await loadAndAssertAddCustomToken({ + ...validParams, + indexCanisterId: mockIndexCanisterId + }); + + expect(result).toEqual({ result: 'error' }); + + expect(spyToastsError).toHaveBeenNthCalledWith(1, { + msg: { text: get(i18n).tokens.import.error.no_metadata } + }); + }); + + it('should return error if token already exits', async () => { + spyLedgerId = indexCanisterMock.ledgerId.mockResolvedValue( + Principal.fromText(mockLedgerCanisterId) + ); + + spyGetTransactions = indexCanisterMock.getTransactions.mockResolvedValue({ + balance: 100n, + transactions: [], + oldest_tx_id: [0n] + }); + + spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([ + ['icrc1:name', { Text: tokenName }], + ['icrc1:symbol', { Text: tokenSymbol }], + ['icrc1:decimals', { Nat: BigInt(tokenDecimals) }], + ['icrc1:fee', { Nat: tokenFee }] + ]); + + const result = await loadAndAssertAddCustomToken({ + ...validParams, + indexCanisterId: mockIndexCanisterId, + icrcTokens: [existingToken] + }); + + expect(result).toEqual({ result: 'error' }); + + expect(spyToastsError).toHaveBeenNthCalledWith(1, { + msg: { text: get(i18n).tokens.error.duplicate_metadata } + }); }); }); }); describe('success', () => { beforeEach(() => { - spyLedgerId = indexCanisterMock.ledgerId.mockResolvedValue( - Principal.fromText(mockLedgerCanisterId) - ); - - spyGetTransactions = indexCanisterMock.getTransactions.mockResolvedValue({ - balance: 100n, - transactions: [], - oldest_tx_id: [0n] - }); - spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([ ['icrc1:name', { Text: tokenName }], ['icrc1:symbol', { Text: tokenSymbol }], @@ -209,15 +247,48 @@ describe('ic-add-custom-tokens.service', () => { ]); }); - it('should accept loading without indexCanisterId', async () => { + const expectedBalance = 100n; + + type LoadAndAssertAddCustomTokenParams = Partial & { + identity: OptionIdentity; + icrcTokens: IcToken[]; + }; + + const assertUpdateCallMetadata = async (params: LoadAndAssertAddCustomTokenParams) => { + await loadAndAssertAddCustomToken(params); + + expect(spyMetadata).toHaveBeenNthCalledWith(1, { + certified: true + }); + }; + + const assertLoadToken = async (params: LoadAndAssertAddCustomTokenParams) => { + const result = await loadAndAssertAddCustomToken(params); + + expect(result.result).toBe('success'); + expect(result.data).toBeDefined(); + expect(result.data?.balance).toBe(expectedBalance); + expect(result.data?.token).toMatchObject({ + name: tokenName, + symbol: tokenSymbol, + decimals: tokenDecimals + }); + }; + + const assertLoadTokenDifferent = async (params: LoadAndAssertAddCustomTokenParams) => { const { result } = await loadAndAssertAddCustomToken({ - identity: mockIdentity, - icrcTokens: [], - ledgerCanisterId: mockLedgerCanisterId + ...params, + icrcTokens: [ + { + ...existingToken, + name: 'Another name', + symbol: 'Another symbol' + } + ] }); expect(result).toBe('success'); - }); + }; it('should init ledger with expected canister id', async () => { await loadAndAssertAddCustomToken(validParams); @@ -228,68 +299,100 @@ describe('ic-add-custom-tokens.service', () => { ); }); - it('should init index with expected canister id', async () => { - await loadAndAssertAddCustomToken(validParams); + describe('without index canister', () => { + beforeEach(() => { + spyBalance = ledgerCanisterMock.balance.mockResolvedValue(expectedBalance); + }); - expect(spyIndexCreate).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ canisterId: Principal.fromText(mockIndexCanisterId) }) - ); - }); + it('should accept loading without indexCanisterId', async () => { + const { result } = await loadAndAssertAddCustomToken({ + identity: mockIdentity, + icrcTokens: [], + ledgerCanisterId: mockLedgerCanisterId + }); - it('should call with an update ledgerId to ensure Index and Ledger are related', async () => { - await loadAndAssertAddCustomToken(validParams); + expect(result).toBe('success'); + }); - expect(spyLedgerId).toHaveBeenNthCalledWith(1, { - certified: true + it('should call with an update balance to retrieve the current balance of the token', async () => { + await loadAndAssertAddCustomToken(validParams); + + expect(spyBalance).toHaveBeenNthCalledWith(1, { + certified: true, + owner: mockPrincipal + }); }); - }); - it('should call with an update getTransactions to retrieve the current balance of the token', async () => { - await loadAndAssertAddCustomToken(validParams); + it('should call with an update metadata to retrieve the details of the token', async () => { + await assertUpdateCallMetadata(validParams); + }); - expect(spyGetTransactions).toHaveBeenNthCalledWith(1, { - account: getIcrcAccount(mockIdentity.getPrincipal()), - certified: true, - max_results: 0n, - start: undefined + it('should successfully load a new token', async () => { + await assertLoadToken(validParams); + }); + + it('should successfully load a new token if name and symbol is different', async () => { + await assertLoadTokenDifferent(validParams); }); }); - it('should call with an update metadata to retrieve the details of the token', async () => { - await loadAndAssertAddCustomToken(validParams); + describe('with index canister', () => { + const validParamsWithIndex = { + ...validParams, + indexCanisterId: mockIndexCanisterId + }; + + beforeEach(() => { + spyLedgerId = indexCanisterMock.ledgerId.mockResolvedValue( + Principal.fromText(mockLedgerCanisterId) + ); + + spyGetTransactions = indexCanisterMock.getTransactions.mockResolvedValue({ + balance: expectedBalance, + transactions: [], + oldest_tx_id: [0n] + }); + }); - expect(spyMetadata).toHaveBeenNthCalledWith(1, { - certified: true + it('should init index with expected canister id', async () => { + await loadAndAssertAddCustomToken(validParamsWithIndex); + + expect(spyIndexCreate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ canisterId: Principal.fromText(mockIndexCanisterId) }) + ); }); - }); - it('should successfully load a new token', async () => { - const result = await loadAndAssertAddCustomToken(validParams); + it('should call with an update ledgerId to ensure Index and Ledger are related', async () => { + await loadAndAssertAddCustomToken(validParamsWithIndex); - expect(result.result).toBe('success'); - expect(result.data).toBeDefined(); - expect(result.data?.balance).toBe(100n); - expect(result.data?.token).toMatchObject({ - name: 'Test Token', - symbol: 'TEST', - decimals: 8 + expect(spyLedgerId).toHaveBeenNthCalledWith(1, { + certified: true + }); }); - }); - it('should successfully load a new token if name and symbol is different', async () => { - const { result } = await loadAndAssertAddCustomToken({ - ...validParams, - icrcTokens: [ - { - ...existingToken, - name: 'Another name', - symbol: 'Another symbol' - } - ] + it('should call with an update getTransactions to retrieve the current balance of the token', async () => { + await loadAndAssertAddCustomToken(validParamsWithIndex); + + expect(spyGetTransactions).toHaveBeenNthCalledWith(1, { + account: getIcrcAccount(mockIdentity.getPrincipal()), + certified: true, + max_results: 0n, + start: undefined + }); }); - expect(result).toBe('success'); + it('should call with an update metadata to retrieve the details of the token', async () => { + await assertUpdateCallMetadata(validParamsWithIndex); + }); + + it('should successfully load a new token', async () => { + await assertLoadToken(validParamsWithIndex); + }); + + it('should successfully load a new token if name and symbol is different', async () => { + await assertLoadTokenDifferent(validParamsWithIndex); + }); }); }); }); From 0664fc6737606d5695e805b55fe2dbbf91739046 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 7 Nov 2024 07:33:57 +0100 Subject: [PATCH 18/24] feat: add todos --- .../src/icp/services/icrc.services.ts | 1 + .../services/custom-token.services.spec.ts | 228 ++++++++++-------- 2 files changed, 128 insertions(+), 101 deletions(-) diff --git a/src/frontend/src/icp/services/icrc.services.ts b/src/frontend/src/icp/services/icrc.services.ts index 2807bbc6ad..95a04692b1 100644 --- a/src/frontend/src/icp/services/icrc.services.ts +++ b/src/frontend/src/icp/services/icrc.services.ts @@ -152,6 +152,7 @@ const loadCustomIcrcTokensData = async ({ const indexCanisterId = fromNullable(index_id); + // TODO(OISY-296): remove isNullish(indexCanisterId) when support for reading balance and no index is fully implemented // Index canister ID currently mandatory in Oisy's frontend if (isNullish(indexCanisterId)) { return undefined; diff --git a/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts b/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts index b3046a8f3a..fe95e20e65 100644 --- a/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts +++ b/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts @@ -1,3 +1,4 @@ +import { IC_CKBTC_INDEX_CANISTER_ID } from '$env/networks.icrc.env'; import { autoLoadCustomToken } from '$icp-eth/services/custom-token.services'; import { icrcCustomTokensStore } from '$icp/stores/icrc-custom-tokens.store'; import type { IcrcCustomToken } from '$icp/types/icrc-custom-token'; @@ -12,6 +13,7 @@ import { mockValidToken } from '$tests/mocks/tokens.mock'; import type { HttpAgent } from '@dfinity/agent'; import { IcrcLedgerCanister } from '@dfinity/ledger-icrc'; import { Principal } from '@dfinity/principal'; +import { isNullish } from '@dfinity/utils'; import { get } from 'svelte/store'; import { expect, type MockInstance } from 'vitest'; import { mock } from 'vitest-mock-extended'; @@ -58,10 +60,12 @@ describe('custom-token.services', () => { describe('success', () => { const assertSetCustomToken = async ({ customTokens, - expectedVersion + expectedVersion, + indexCanisterId }: { customTokens: IcrcCustomToken[]; expectedVersion: [] | [bigint]; + indexCanisterId: string | undefined; }) => { const spySetCustomToken = backendCanisterMock.setCustomToken.mockResolvedValue(undefined); const spyListCustomTokens = backendCanisterMock.listCustomTokens.mockResolvedValue([]); @@ -72,8 +76,11 @@ describe('custom-token.services', () => { standard: 'erc20' as const }; + const [first, ...rest] = customTokens; + const icrcCustomTokens = [{ ...first, indexCanisterId }, ...rest]; + const { result } = await autoLoadCustomToken({ - icrcCustomTokens: customTokens, + icrcCustomTokens, sendToken: mockSendToken, identity: mockIdentity }); @@ -86,7 +93,7 @@ describe('custom-token.services', () => { version: expectedVersion, token: { Icrc: { - index_id: [Principal.fromText(mockSendToken.indexCanisterId)], + index_id: isNullish(indexCanisterId) ? [] : [Principal.fromText(indexCanisterId)], ledger_id: Principal.fromText(mockSendToken.ledgerCanisterId) } } @@ -96,77 +103,92 @@ describe('custom-token.services', () => { expect(spyListCustomTokens).toHaveBeenCalledWith({ certified: true }); }; - it('should call setCustomToken with a new custom token', async () => { - await assertSetCustomToken({ customTokens: mockIcrcCustomTokens, expectedVersion: [] }); - }); - - it('should call setCustomToken to update a custom token', async () => { - const customTokens: IcrcCustomToken[] = [ - { - ...mockIcrcCustomTokens[0], - version: 1n - }, - mockIcrcCustomTokens[1] - ]; - - await assertSetCustomToken({ - customTokens, - expectedVersion: [customTokens[0].version ?? 0n] - }); - }); - - it('should load tokens after set custom token', async () => { - backendCanisterMock.setCustomToken.mockResolvedValue(undefined); - const spyListCustomTokens = backendCanisterMock.listCustomTokens.mockResolvedValue([ - { - token: { - Icrc: { - index_id: [Principal.fromText(mockValidSendToken.indexCanisterId)], - ledger_id: Principal.fromText(mockValidSendToken.ledgerCanisterId) - } - }, - version: [1n], - enabled: true - } - ]); - - const spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([ - ['icrc1:name', { Text: mockValidSendToken.name }], - ['icrc1:symbol', { Text: mockValidSendToken.symbol }], - ['icrc1:decimals', { Nat: BigInt(mockValidSendToken.decimals) }], - ['icrc1:fee', { Nat: mockValidSendToken.fee }] - ]); - - const { result } = await autoLoadCustomToken({ - icrcCustomTokens: mockIcrcCustomTokens, - sendToken: mockValidSendToken, - identity: mockIdentity - }); - - expect(result).toBe('loaded'); - - expect(spyListCustomTokens).toHaveBeenCalledWith({ certified: true }); - - expect(spyMetadata).toHaveBeenCalledWith({ certified: true }); - - const store = get(icrcCustomTokensStore); - - expect(store).toHaveLength(1); - expect(store).toEqual([ - { - certified: true, - data: expect.objectContaining({ - ...mockValidIcToken, - id: expect.any(Symbol), - category: 'custom', - position: 4, - enabled: true, - standard: 'icrc', + it.each([undefined, IC_CKBTC_INDEX_CANISTER_ID])( + 'should call setCustomToken with a new custom token with index %s', + async (indexCanisterId) => { + await assertSetCustomToken({ + customTokens: mockIcrcCustomTokens, + expectedVersion: [], + indexCanisterId + }); + } + ); + + it.each([undefined, IC_CKBTC_INDEX_CANISTER_ID])( + 'should call setCustomToken to update a custom token with index %s', + async (indexCanisterId) => { + const customTokens: IcrcCustomToken[] = [ + { + ...mockIcrcCustomTokens[0], version: 1n - }) - } - ]); - }); + }, + mockIcrcCustomTokens[1] + ]; + + await assertSetCustomToken({ + customTokens, + expectedVersion: [customTokens[0].version ?? 0n], + indexCanisterId + }); + } + ); + + // TODO(OISY-296): introduce test for undefined Index canister ID + it.each([IC_CKBTC_INDEX_CANISTER_ID])( + 'should load tokens after set custom token with index ID %s', + async (indexCanisterId) => { + backendCanisterMock.setCustomToken.mockResolvedValue(undefined); + const spyListCustomTokens = backendCanisterMock.listCustomTokens.mockResolvedValue([ + { + token: { + Icrc: { + index_id: isNullish(indexCanisterId) ? [] : [Principal.fromText(indexCanisterId)], + ledger_id: Principal.fromText(mockValidSendToken.ledgerCanisterId) + } + }, + version: [1n], + enabled: true + } + ]); + + const spyMetadata = ledgerCanisterMock.metadata.mockResolvedValue([ + ['icrc1:name', { Text: mockValidSendToken.name }], + ['icrc1:symbol', { Text: mockValidSendToken.symbol }], + ['icrc1:decimals', { Nat: BigInt(mockValidSendToken.decimals) }], + ['icrc1:fee', { Nat: mockValidSendToken.fee }] + ]); + + const { result } = await autoLoadCustomToken({ + icrcCustomTokens: mockIcrcCustomTokens, + sendToken: mockValidSendToken, + identity: mockIdentity + }); + + expect(result).toBe('loaded'); + + expect(spyListCustomTokens).toHaveBeenCalledWith({ certified: true }); + + expect(spyMetadata).toHaveBeenCalledWith({ certified: true }); + + const store = get(icrcCustomTokensStore); + + expect(store).toHaveLength(1); + expect(store).toEqual([ + { + certified: true, + data: expect.objectContaining({ + ...mockValidIcToken, + id: expect.any(Symbol), + category: 'custom', + position: 4, + enabled: true, + standard: 'icrc', + version: 1n + }) + } + ]); + } + ); }); describe('error', () => { @@ -210,38 +232,42 @@ describe('custom-token.services', () => { }); }); - it('should result with loaded but toastError if metadata fails', async () => { - backendCanisterMock.setCustomToken.mockResolvedValue(undefined); - - backendCanisterMock.listCustomTokens.mockResolvedValue([ - { - token: { - Icrc: { - index_id: [Principal.fromText(mockValidSendToken.indexCanisterId)], - ledger_id: Principal.fromText(mockValidSendToken.ledgerCanisterId) - } - }, - version: [1n], - enabled: true - } - ]); + // TODO(OISY-296): introduce test for undefined Index canister ID + it.each([IC_CKBTC_INDEX_CANISTER_ID])( + 'should result with loaded but toastError if metadata fails with index ID %s', + async (indexCanisterId) => { + backendCanisterMock.setCustomToken.mockResolvedValue(undefined); + + backendCanisterMock.listCustomTokens.mockResolvedValue([ + { + token: { + Icrc: { + index_id: isNullish(indexCanisterId) ? [] : [Principal.fromText(indexCanisterId)], + ledger_id: Principal.fromText(mockValidSendToken.ledgerCanisterId) + } + }, + version: [1n], + enabled: true + } + ]); - const err = new Error('test'); - ledgerCanisterMock.metadata.mockRejectedValue(err); + const err = new Error('test'); + ledgerCanisterMock.metadata.mockRejectedValue(err); - const { result } = await autoLoadCustomToken({ - icrcCustomTokens: mockIcrcCustomTokens, - sendToken: mockValidSendToken, - identity: mockIdentity - }); + const { result } = await autoLoadCustomToken({ + icrcCustomTokens: mockIcrcCustomTokens, + sendToken: mockValidSendToken, + identity: mockIdentity + }); - expect(result).toBe('loaded'); + expect(result).toBe('loaded'); - expect(spyToastsError).toHaveBeenNthCalledWith(1, { - msg: { text: get(i18n).init.error.icrc_canisters }, - err - }); - }); + expect(spyToastsError).toHaveBeenNthCalledWith(1, { + msg: { text: get(i18n).init.error.icrc_canisters }, + err + }); + } + ); }); }); }); From 066e7e353048cb9d9362841d9fcca351bbe040af Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 7 Nov 2024 07:35:10 +0100 Subject: [PATCH 19/24] docs: jira number --- src/frontend/src/icp/workers/icrc-wallet.worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/icp/workers/icrc-wallet.worker.ts b/src/frontend/src/icp/workers/icrc-wallet.worker.ts index c0cf1366b7..1d10039eb9 100644 --- a/src/frontend/src/icp/workers/icrc-wallet.worker.ts +++ b/src/frontend/src/icp/workers/icrc-wallet.worker.ts @@ -25,7 +25,7 @@ const getTransactions = ({ }: SchedulerJobParams): Promise => { assertNonNullish(data, 'No data - indexCanisterId - provided to fetch transactions.'); - // TODO: This is not clean. If the index canister ID is not provided we should not even land here. + // TODO(OISY-296): This is not clean. If the index canister ID is not provided we should not even land here. const { indexCanisterId } = data; assertNonNullish(indexCanisterId); From 332873a1a26c45662c1c33eda115ad8d5498512f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 07:24:19 +0000 Subject: [PATCH 20/24] =?UTF-8?q?=F0=9F=A4=96=20Apply=20formatting=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icp-eth/services/custom-token.services.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts b/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts index 6072b6d219..31e1101740 100644 --- a/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts +++ b/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts @@ -1,5 +1,10 @@ -import {IC_CKBTC_INDEX_CANISTER_ID, IC_CKBTC_LEDGER_CANISTER_ID} from '$env/networks.icrc.env'; -import {autoLoadCustomToken, setCustomToken, toCustomToken} from '$icp-eth/services/custom-token.services'; +import { IC_CKBTC_INDEX_CANISTER_ID, IC_CKBTC_LEDGER_CANISTER_ID } from '$env/networks.icrc.env'; +import { + autoLoadCustomToken, + setCustomToken, + toCustomToken +} from '$icp-eth/services/custom-token.services'; +import type { SaveCustomToken } from '$icp/services/ic-custom-tokens.services'; import { icrcCustomTokensStore } from '$icp/stores/icrc-custom-tokens.store'; import type { IcrcCustomToken } from '$icp/types/icrc-custom-token'; import * as agent from '$lib/actors/agents.ic'; @@ -7,17 +12,16 @@ import { BackendCanister } from '$lib/canisters/backend.canister'; import { i18n } from '$lib/stores/i18n.store'; import * as toastsStore from '$lib/stores/toasts.store'; import { mockValidIcToken } from '$tests/mocks/ic-tokens.mock'; -import {mockIcrcCustomToken, mockIcrcCustomTokens} from '$tests/mocks/icrc-custom-tokens.mock'; +import { mockIcrcCustomToken, mockIcrcCustomTokens } from '$tests/mocks/icrc-custom-tokens.mock'; import { mockIdentity } from '$tests/mocks/identity.mock'; import { mockValidToken } from '$tests/mocks/tokens.mock'; import type { HttpAgent } from '@dfinity/agent'; import { IcrcLedgerCanister } from '@dfinity/ledger-icrc'; import { Principal } from '@dfinity/principal'; -import {isNullish, toNullable} from '@dfinity/utils'; +import { isNullish, toNullable } from '@dfinity/utils'; import { get } from 'svelte/store'; import { expect, type MockInstance } from 'vitest'; import { mock } from 'vitest-mock-extended'; -import type {SaveCustomToken} from "$icp/services/ic-custom-tokens.services"; describe('custom-token.services', () => { const backendCanisterMock = mock(); From c2da13a2bffb7b4e23474cd56586f67767b3e0d7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 7 Nov 2024 08:35:56 +0100 Subject: [PATCH 21/24] test: update for optional index canister id --- .../services/custom-token.services.spec.ts | 109 +++++++++++------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts b/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts index 6072b6d219..49f3c6f1ea 100644 --- a/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts +++ b/src/frontend/src/tests/icp-eth/services/custom-token.services.spec.ts @@ -1,5 +1,10 @@ -import {IC_CKBTC_INDEX_CANISTER_ID, IC_CKBTC_LEDGER_CANISTER_ID} from '$env/networks.icrc.env'; -import {autoLoadCustomToken, setCustomToken, toCustomToken} from '$icp-eth/services/custom-token.services'; +import { IC_CKBTC_INDEX_CANISTER_ID, IC_CKBTC_LEDGER_CANISTER_ID } from '$env/networks.icrc.env'; +import { + autoLoadCustomToken, + setCustomToken, + toCustomToken +} from '$icp-eth/services/custom-token.services'; +import type { SaveCustomToken } from '$icp/services/ic-custom-tokens.services'; import { icrcCustomTokensStore } from '$icp/stores/icrc-custom-tokens.store'; import type { IcrcCustomToken } from '$icp/types/icrc-custom-token'; import * as agent from '$lib/actors/agents.ic'; @@ -7,17 +12,16 @@ import { BackendCanister } from '$lib/canisters/backend.canister'; import { i18n } from '$lib/stores/i18n.store'; import * as toastsStore from '$lib/stores/toasts.store'; import { mockValidIcToken } from '$tests/mocks/ic-tokens.mock'; -import {mockIcrcCustomToken, mockIcrcCustomTokens} from '$tests/mocks/icrc-custom-tokens.mock'; +import { mockIcrcCustomToken, mockIcrcCustomTokens } from '$tests/mocks/icrc-custom-tokens.mock'; import { mockIdentity } from '$tests/mocks/identity.mock'; import { mockValidToken } from '$tests/mocks/tokens.mock'; import type { HttpAgent } from '@dfinity/agent'; import { IcrcLedgerCanister } from '@dfinity/ledger-icrc'; import { Principal } from '@dfinity/principal'; -import {isNullish, toNullable} from '@dfinity/utils'; +import { isNullish, toNullable } from '@dfinity/utils'; import { get } from 'svelte/store'; import { expect, type MockInstance } from 'vitest'; import { mock } from 'vitest-mock-extended'; -import type {SaveCustomToken} from "$icp/services/ic-custom-tokens.services"; describe('custom-token.services', () => { const backendCanisterMock = mock(); @@ -273,53 +277,70 @@ describe('custom-token.services', () => { }); describe('toCustomToken', () => { - it.each([undefined, 2n])('should convert to CustomToken with version %s', (version) => { - const input: SaveCustomToken = { - enabled: true, - version, - ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID, - indexCanisterId: IC_CKBTC_INDEX_CANISTER_ID - }; + describe.each([undefined, IC_CKBTC_INDEX_CANISTER_ID])( + 'with index ID %s', + (indexCanisterId) => { + it.each([undefined, 2n])('should convert to CustomToken with version %s', (version) => { + const input: SaveCustomToken = { + enabled: true, + version, + ledgerCanisterId: IC_CKBTC_LEDGER_CANISTER_ID, + indexCanisterId + }; - const result = toCustomToken(input); + const result = toCustomToken(input); - expect(result).toEqual({ - enabled: input.enabled, - version: toNullable(version), - token: { - Icrc: { - ledger_id: Principal.fromText(input.ledgerCanisterId), - index_id: [Principal.fromText(input.indexCanisterId)] - } - } - }); - }); + expect(result).toEqual({ + enabled: input.enabled, + version: toNullable(version), + token: { + Icrc: { + ledger_id: Principal.fromText(input.ledgerCanisterId), + index_id: toNullable( + isNullish(indexCanisterId) ? undefined : Principal.fromText(indexCanisterId) + ) + } + } + }); + }); + } + ); }); describe('setCustomToken', () => { - it('should call backend setCustomToken with expected parameters', async () => { - const spySetCustomToken = backendCanisterMock.setCustomToken.mockResolvedValue(undefined); + describe.each([undefined, IC_CKBTC_INDEX_CANISTER_ID])( + 'with index ID %s', + (indexCanisterId) => { + it('should call backend setCustomToken with expected parameters', async () => { + const spySetCustomToken = backendCanisterMock.setCustomToken.mockResolvedValue(undefined); - const enabled = true; + const enabled = true; - await setCustomToken({ - token: mockIcrcCustomToken, - identity: mockIdentity, - enabled - }); + await setCustomToken({ + token: { + ...mockIcrcCustomToken, + indexCanisterId + }, + identity: mockIdentity, + enabled + }); - expect(spySetCustomToken).toHaveBeenCalledWith({ - token: { - enabled, - version: toNullable(mockIcrcCustomToken.version), - token: { - Icrc: { - ledger_id: Principal.fromText(mockIcrcCustomToken.ledgerCanisterId), - index_id: [Principal.fromText(mockIcrcCustomToken.indexCanisterId)] + expect(spySetCustomToken).toHaveBeenCalledWith({ + token: { + enabled, + version: toNullable(mockIcrcCustomToken.version), + token: { + Icrc: { + ledger_id: Principal.fromText(mockIcrcCustomToken.ledgerCanisterId), + index_id: toNullable( + isNullish(indexCanisterId) ? undefined : Principal.fromText(indexCanisterId) + ) + } + } } - } - } - }); - }); + }); + }); + } + ); }); }); From ebea89ad354adf01ca773ac4bc46ddfe2083819c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 7 Nov 2024 15:04:43 +0100 Subject: [PATCH 22/24] feat: invert --- .../src/icp/services/ic-add-custom-tokens.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts b/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts index 976ce77cd5..c51b37324f 100644 --- a/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts +++ b/src/frontend/src/icp/services/ic-add-custom-tokens.service.ts @@ -6,7 +6,7 @@ import { i18n } from '$lib/stores/i18n.store'; import { toastsError } from '$lib/stores/toasts.store'; import type { OptionIdentity } from '$lib/types/identity'; import type { Identity } from '@dfinity/agent'; -import { assertNonNullish, isNullish } from '@dfinity/utils'; +import { assertNonNullish, isNullish, nonNullish } from '@dfinity/utils'; import { get } from 'svelte/store'; export interface ValidateTokenData { @@ -46,13 +46,13 @@ export const loadAndAssertAddCustomToken = async ({ return { result: 'error' }; } - const { valid } = isNullish(indexCanisterId) - ? { valid: true } - : await assertIndexLedgerId({ + const { valid } = nonNullish(indexCanisterId) + ? await assertIndexLedgerId({ identity, ...canisterIds, indexCanisterId - }); + }) + : { valid: true }; if (!valid) { return { result: 'error' }; From 0bb96909e4dea6f0878355f9dcc54d6ef6d0d661 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 7 Nov 2024 15:10:00 +0100 Subject: [PATCH 23/24] refactor: rename local mock variable --- .../src/tests/icp/validation/ic-token.validation.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts index 641d3c7ecf..d1ff8c0f73 100644 --- a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -10,7 +10,7 @@ import { mockValidIcToken } from '$tests/mocks/ic-tokens.mock'; import { mockValidToken } from '$tests/mocks/tokens.mock'; describe('ic-token.validation', () => { - const validIcTokenWithIndex: IcToken = { + const mockValidIcTokenWithIndex: IcToken = { ...mockValidIcToken, indexCanisterId: IC_CKBTC_INDEX_CANISTER_ID }; @@ -37,7 +37,7 @@ describe('ic-token.validation', () => { describe('isIcTokenCanistersStrict', () => { it('should return true for a valid IcToken with IcCanistersStrict', () => { - expect(isIcTokenCanistersStrict(validIcTokenWithIndex)).toBe(true); + expect(isIcTokenCanistersStrict(mockValidIcTokenWithIndex)).toBe(true); }); it('should return false for a valid IcToken without strict canisters fields', () => { @@ -51,7 +51,7 @@ describe('ic-token.validation', () => { describe('isNotIcTokenCanistersStrict', () => { it('should return false for a valid IcToken with IcCanistersStrict', () => { - expect(isNotIcTokenCanistersStrict(validIcTokenWithIndex)).toBe(false); + expect(isNotIcTokenCanistersStrict(mockValidIcTokenWithIndex)).toBe(false); }); it('should return true for a valid IcToken without strict canisters fields', () => { From 9854ef0db163bbf3ab2787a877f8f7560bd07520 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:12:32 +0000 Subject: [PATCH 24/24] =?UTF-8?q?=F0=9F=A4=96=20Apply=20formatting=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/src/tests/mocks/ic-tokens.mock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/tests/mocks/ic-tokens.mock.ts b/src/frontend/src/tests/mocks/ic-tokens.mock.ts index e8a3ed8ea2..2e5a7b4562 100644 --- a/src/frontend/src/tests/mocks/ic-tokens.mock.ts +++ b/src/frontend/src/tests/mocks/ic-tokens.mock.ts @@ -1,5 +1,5 @@ import { IC_CKBTC_LEDGER_CANISTER_ID } from '$env/networks.icrc.env'; -import type {IcCanisters, IcCkToken, IcToken} from '$icp/types/ic-token'; +import type { IcCanisters, IcCkToken, IcToken } from '$icp/types/ic-token'; import { mockValidToken } from '$tests/mocks/tokens.mock'; export const mockValidIcCanisters: IcCanisters = {