Skip to content

Commit

Permalink
feat(frontend): optional index canister (#3334)
Browse files Browse the repository at this point in the history
# Motivation

Make the index canister ID optional in the core type definition without
changing the way we load information - i.e. in a backwards compatible
manner.

# Notes

This PR is relatively large, so Antonio and I reviewed the code together
offline.

# Changes

- `custom-token.services.ts`: extend `toCustomToken` to support optional
Index canister ID
- `IcTransactions.svelte`: silently ignore listing transactions if no
index
- `ic-token.schema.ts`: set Index Canister as optional in the base type
`IcCanisters`
- `ic-add-custom-tokens.service.ts`: extend support to, in the future,
to allow user to register custom token without index canister id
- `icrc-wallet.worker.ts`: assert index is set. in the future this
should not be called without index
- add few TODOs
- adapt tests

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
peterpeterparker and github-actions[bot] authored Nov 8, 2024
1 parent e23b3bd commit 3a4e4f0
Show file tree
Hide file tree
Showing 15 changed files with 486 additions and 313 deletions.
6 changes: 4 additions & 2 deletions src/frontend/src/icp-eth/services/custom-token.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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
)
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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';
Expand Down Expand Up @@ -69,6 +70,12 @@
return;
}
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;
}
await loadNextTransactions({
owner: $authIdentity.getPrincipal(),
identity: $authIdentity,
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/icp/derived/icrc.derived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const icrcDefaultTokensToggleable: Readable<IcTokenToggleable[]> = derived(
userLedgerCanisterId === ledgerCanisterId && userIndexCanisterId === indexCanisterId
);

return mapDefaultTokenToToggleable({
return mapDefaultTokenToToggleable<IcToken>({
defaultToken: {
ledgerCanisterId,
indexCanisterId,
Expand Down
3 changes: 1 addition & 2 deletions src/frontend/src/icp/schema/ic-token.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
62 changes: 40 additions & 22 deletions src/frontend/src/icp/services/ic-add-custom-tokens.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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-token';
import { mapIcrcToken } from '$icp/utils/icrc.utils';
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 {
Expand Down Expand Up @@ -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({
Expand All @@ -53,22 +46,26 @@ export const loadAndAssertAddCustomToken = async ({
return { result: 'error' };
}

const { valid } = await assertLedgerId({
identity,
...canisterIds
});
const { valid } = nonNullish(indexCanisterId)
? await assertIndexLedgerId({
identity,
...canisterIds,
indexCanisterId
})
: { valid: true };

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)) {
Expand Down Expand Up @@ -157,10 +154,31 @@ const loadMetadata = async ({
}
};

const loadBalance = async ({
const loadLedgerBalance = async ({
identity,
ledgerCanisterId
}: IcCanisters & { identity: Identity }): Promise<bigint> => {
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<IcCanisters, 'indexCanisterId'> & { identity: Identity }): Promise<bigint> => {
}: Required<Pick<IcCanisters, 'indexCanisterId'>> & { identity: Identity }): Promise<bigint> => {
try {
const { balance } = await getTransactionsIcrc({
indexCanisterId,
Expand All @@ -181,11 +199,11 @@ const loadBalance = async ({
}
};

const assertLedgerId = async ({
const assertIndexLedgerId = async ({
identity,
indexCanisterId,
ledgerCanisterId
}: IcCanisters & { identity: Identity }): Promise<{ valid: boolean }> => {
}: Required<IcCanisters> & { identity: Identity }): Promise<{ valid: boolean }> => {
try {
const ledgerId = await getLedgerId({
indexCanisterId,
Expand Down
6 changes: 3 additions & 3 deletions src/frontend/src/icp/services/ic-transactions.services.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -23,7 +23,7 @@ const getTransactions = async ({
identity: OptionIdentity;
start?: bigint;
maxResults?: bigint;
token: IcToken;
token: IcToken & IcCanistersStrict;
}): Promise<IcTransaction[]> => {
if (standard === 'icrc') {
const { transactions } = await getTransactionsIcrc({
Expand All @@ -49,7 +49,7 @@ export const loadNextTransactions = ({
identity: OptionIdentity;
start?: bigint;
maxResults?: bigint;
token: IcToken;
token: IcToken & IcCanistersStrict;
signalEnd: () => void;
}): Promise<void> =>
queryAndUpdate<IcTransaction[]>({
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/icp/services/icrc.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/frontend/src/icp/workers/icrc-wallet.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,18 @@ const getTransactions = ({
}: SchedulerJobParams<PostMessageDataRequestIcrc>): Promise<IcrcIndexNgGetTransactions> => {
assertNonNullish(data, 'No data - indexCanisterId - provided to fetch transactions.');

// 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);

return getTransactionsApi({
identity,
certified,
owner: identity.getPrincipal(),
// We query tip to discover the new transactions
start: undefined,
...data
...data,
indexCanisterId
});
};

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 3a4e4f0

Please sign in to comment.