Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Quick import token #6204

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
54bdf80
poc: import token by url
mstrasinskis Jan 20, 2025
f0cf3ac
Add query param consts
mstrasinskis Jan 21, 2025
f8f6d8c
Derived stores
mstrasinskis Jan 21, 2025
d22061a
Use derived stores
mstrasinskis Jan 21, 2025
a0e4fa6
Refactor: mode prop
mstrasinskis Jan 21, 2025
e90b3f7
Revert "Refactor: mode prop"
mstrasinskis Jan 21, 2025
e558895
Canister ID text validation
mstrasinskis Jan 21, 2025
443248e
Clear query params
mstrasinskis Jan 21, 2025
81dc627
Replace new derived stores with pageStore
mstrasinskis Jan 21, 2025
cb172de
Replace new derived stores with pageStore in modal
mstrasinskis Jan 22, 2025
d084249
Merge branch 'main' into quick-import-token
mstrasinskis Jan 22, 2025
285c8a0
Merge branch 'main' into quick-import-token
mstrasinskis Jan 23, 2025
91a0d38
Merge branch 'main' into quick-import-token
mstrasinskis Jan 27, 2025
bd12eb1
Fill in the import form from the URL
mstrasinskis Jan 27, 2025
429a1e3
test: by URL
mstrasinskis Jan 27, 2025
0bc5e68
test: drop query params
mstrasinskis Jan 27, 2025
119d5f3
Should not auto validate when feature flag disabled
mstrasinskis Jan 27, 2025
34ef066
Labels
mstrasinskis Jan 27, 2025
cfe2061
Avoid double error
mstrasinskis Jan 27, 2025
a6e54fd
test: catches invalid canister ID formats from URL
mstrasinskis Jan 27, 2025
2210919
Merge branch 'import-token-url-ids' into quick-import-token
mstrasinskis Jan 27, 2025
ad4be63
Merge branch 'main' into quick-import-token
mstrasinskis Jan 27, 2025
582d3b6
Merge branch 'main' into quick-import-token
mstrasinskis Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions frontend/src/lib/components/accounts/ImportTokenForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import PrincipalInput from "$lib/components/ui/PrincipalInput.svelte";
import { i18n } from "$lib/stores/i18n";
import { Html, IconOpenInNew } from "@dfinity/gix-components";
import { Html, IconInfo, IconOpenInNew } from "@dfinity/gix-components";
import type { Principal } from "@dfinity/principal";
import { isNullish } from "@dfinity/utils";
import { createEventDispatcher } from "svelte";
import SignIn from "$lib/components/common/SignIn.svelte";
import { authSignedInStore } from "$lib/derived/auth.derived";
import Banner from "$lib/components/ui/Banner.svelte";
import BannerIcon from "$lib/components/ui/BannerIcon.svelte";

export let ledgerCanisterId: Principal | undefined = undefined;
export let indexCanisterId: Principal | undefined = undefined;
Expand All @@ -34,7 +38,9 @@
{$i18n.import_token.doc_link_label}
</a>

<form on:submit|preventDefault={() => dispatch("nnsSubmit")}>
<form
on:submit|preventDefault={() => $authSignedInStore && dispatch("nnsSubmit")}
>
{#if addIndexCanisterMode}
<ImportTokenCanisterId
testId="ledger-canister-id-view"
Expand Down Expand Up @@ -74,10 +80,18 @@
<Html text={$i18n.import_token.index_canister_description} />
</p>

{#if !addIndexCanisterMode}
{#if !addIndexCanisterMode && $authSignedInStore}
<CalloutWarning htmlText={$i18n.import_token.warning} />
{/if}

{#if !$authSignedInStore}
<Banner text="Please sign in to proceed with the token import.">
<BannerIcon slot="icon">
<IconInfo />
</BannerIcon>
</Banner>
{/if}

<div class="toolbar">
<button
class="secondary"
Expand All @@ -88,14 +102,18 @@
{$i18n.core.cancel}
</button>

<button
data-tid="submit-button"
class="primary"
type="submit"
disabled={isSubmitDisabled}
>
{addIndexCanisterMode ? $i18n.core.add : $i18n.core.next}
</button>
{#if $authSignedInStore}
<button
data-tid="submit-button"
class="primary"
type="submit"
disabled={isSubmitDisabled}
>
{addIndexCanisterMode ? $i18n.core.add : $i18n.core.next}
</button>
{:else}
<SignIn />
{/if}
</div>
</form>
</TestIdWrapper>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,8 @@
"is_duplication": "You have already imported this token, you can find it in the token list.",
"is_sns": "You cannot import SNS tokens, they are added by the NNS.",
"is_important": "This token is already in the token list.",
"is_icp": "You cannot import ICP."
"is_icp": "You cannot import ICP.",
"invalid_canister_id": "Importing the token was unsuccessful because \"$canisterId\" is not a valid canister ID. Please verify the ID and retry."
},
"error__sns": {
"undefined_project": "The requested project is invalid or throws an error.",
Expand Down
95 changes: 90 additions & 5 deletions frontend/src/lib/modals/accounts/ImportTokenModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
import { matchLedgerIndexPair } from "$lib/services/icrc-index.services";
import { addImportedToken } from "$lib/services/imported-tokens.services";
import { startBusy, stopBusy } from "$lib/stores/busy.store";
import { DISABLE_IMPORT_TOKEN_VALIDATION_FOR_TESTING } from "$lib/stores/feature-flags.store";
import {
DISABLE_IMPORT_TOKEN_VALIDATION_FOR_TESTING,
ENABLE_IMPORT_TOKEN_BY_URL,
} from "$lib/stores/feature-flags.store";
import { i18n } from "$lib/stores/i18n";
import { importedTokensStore } from "$lib/stores/imported-tokens.store";
import { toastsError } from "$lib/stores/toasts.store";
import { toastsError, toastsShow } from "$lib/stores/toasts.store";
import type { IcrcTokenMetadata } from "$lib/types/icrc";
import { isImportantCkToken } from "$lib/utils/icrc-tokens.utils";
import { isImportedToken } from "$lib/utils/imported-tokens.utils";
Expand All @@ -22,10 +25,13 @@
type WizardStep,
type WizardSteps,
} from "@dfinity/gix-components";
import type { Principal } from "@dfinity/principal";
import { Principal } from "@dfinity/principal";
import { isNullish, nonNullish } from "@dfinity/utils";
import { createEventDispatcher } from "svelte";
import { get } from "svelte/store";
import { authSignedInStore } from "$lib/derived/auth.derived";
import { AppPath } from "$lib/constants/routes.constants";
import { pageStore } from "$lib/derived/page.derived";

let currentStep: WizardStep | undefined = undefined;

Expand All @@ -51,6 +57,53 @@
let indexCanisterId: Principal | undefined;
let tokenMetaData: IcrcTokenMetadata | undefined;

const validateCanisterIdText = (canisterIdText: string): boolean => {
try {
return Boolean(Principal.fromText(canisterIdText));
} catch (err) {
console.error(err);
toastsError({
labelKey: "error__imported_tokens.invalid_canister_id",
substitutions: { $canisterId: canisterIdText },
});
close();
goto(AppPath.Tokens);
return false;
}
};
$: {
const ledgerId = $pageStore.importTokenLedgerId;
if (
nonNullish(ledgerId) &&
isNullish(ledgerCanisterId) &&
validateCanisterIdText(ledgerId)
) {
ledgerCanisterId = Principal.fromText(ledgerId);
}
}
$: {
const indexId = $pageStore.importTokenIndexId;
if (
nonNullish(indexId) &&
isNullish(indexCanisterId) &&
validateCanisterIdText(indexId)
) {
indexCanisterId = Principal.fromText(indexId);
}
}

let isAutoSubmitDone = false;
$: if (
$authSignedInStore &&
$ENABLE_IMPORT_TOKEN_BY_URL &&
!isAutoSubmitDone &&
// Wait for the imported tokens to be loaded (for successful validation).
nonNullish($importedTokensStore?.importedTokens)
) {
isAutoSubmitDone = true;
onSubmit();
}

const getTokenMetaData = async (
ledgerCanisterId: Principal
): Promise<IcrcTokenMetadata | undefined> => {
Expand All @@ -63,6 +116,29 @@
});
}
};
const checkAlreadyImported = (ledgerCanisterId: Principal): boolean => {
if (
isImportedToken({
ledgerCanisterId,
importedTokens: $importedTokensStore?.importedTokens,
})
) {
toastsShow({
level: "warn",
labelKey: "error__imported_tokens.is_duplication",
});

goto(
buildWalletUrl({
universe: ledgerCanisterId.toText(),
})
);

return false;
}

return true;
};
const validateLedgerCanister = (
ledgerCanisterId: Principal
): { errorLabelKey: string | undefined } => {
Expand Down Expand Up @@ -111,6 +187,9 @@
return;
}

if (!checkAlreadyImported(ledgerCanisterId)) {
return;
}
const { errorLabelKey } = validateLedgerCanister(ledgerCanisterId);
if (nonNullish(errorLabelKey)) {
toastsError({
Expand Down Expand Up @@ -163,7 +242,7 @@
importedTokens: $importedTokensStore.importedTokens,
});
if (success) {
dispatch("nnsClose");
close();
goto(
buildWalletUrl({
universe: ledgerCanisterId.toText(),
Expand All @@ -174,14 +253,20 @@
stopBusy("import-token-importing");
}
};

const close = () => dispatch("nnsClose");
</script>

<WizardModal
testId="import-token-modal-component"
{steps}
bind:currentStep
bind:this={modal}
on:nnsClose
on:nnsClose={() => {
close();
// Navigate on close to clear all query parameters.
goto(AppPath.Tokens);
}}
>
<svelte:fragment slot="title">{currentStep?.title}</svelte:fragment>

Expand Down
16 changes: 16 additions & 0 deletions frontend/src/lib/pages/SignInTokens.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@
import { i18n } from "$lib/stores/i18n";
import type { UserToken } from "$lib/types/tokens-page";
import { IconAccountsPage, PageBanner } from "@dfinity/gix-components";
import { nonNullish } from "@dfinity/utils";
import ImportTokenModal from "$lib/modals/accounts/ImportTokenModal.svelte";
import { authSignedInStore } from "$lib/derived/auth.derived";
import { ENABLE_IMPORT_TOKEN_BY_URL } from "$lib/stores/feature-flags.store";
import { pageStore } from "$lib/derived/page.derived";

export let userTokensData: UserToken[];

// Since there are two ImportTokenModals on both Tokens and SignInTokens pages,
// we need to hide this modal after a successful sign-in to
// prevent it from blocking this component’s destruction.
let showImportTokenModal = false;
$: showImportTokenModal =
!$authSignedInStore && nonNullish($pageStore.importTokenLedgerId);
</script>

<div class="content" data-tid="sign-in-tokens-page-component">
Expand All @@ -21,6 +33,10 @@
{userTokensData}
firstColumnHeader={$i18n.tokens.projects_header}
/>

{#if showImportTokenModal && $ENABLE_IMPORT_TOKEN_BY_URL}
<ImportTokenModal on:nnsClose={() => (showImportTokenModal = false)} />
{/if}
</div>

<style lang="scss">
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/pages/Tokens.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";

Check warning on line 2 in frontend/src/lib/pages/Tokens.svelte

View workflow job for this annotation

GitHub Actions / svelte-lint

'page' is defined but never used. Allowed unused vars must match /^_/u
import HideZeroBalancesToggle from "$lib/components/tokens/TokensTable/HideZeroBalancesToggle.svelte";
import TokensTable from "$lib/components/tokens/TokensTable/TokensTable.svelte";
import UsdValueBanner from "$lib/components/ui/UsdValueBanner.svelte";
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ interface I18nError__imported_tokens {
is_sns: string;
is_important: string;
is_icp: string;
invalid_canister_id: string;
}

interface I18nError__sns {
Expand Down
Loading
Loading