Skip to content

Commit

Permalink
Add index canister (#5400)
Browse files Browse the repository at this point in the history
# Motivation

Users can import a custom token without providing an index canister ID.
It should be possible to add the canister afterwards, and this PR
addresses that.

**Note:** the transactions are loaded automatically by the transaction
sync mechanism.

**Demo:**
https://qsgjb-riaaa-aaaaa-aaaga-cai.mstr-ingress.devenv.dfinity.network
ckRED token for testing:
- ledger canister ID: `mrfq3-7eaaa-aaaaa-qabja-cai`
- index canister ID: `mwewp-s4aaa-aaaaa-qabjq-cai`

# Changes

- Display "Add index canister" button and description.
- Add `AddIndexCanisterModal` component.
- Add index canister logic.
- New mode `addIndexCanisterMode` to import token form.
- Expose `disabled` prop of PrincipalInput.

# Tests

- Added.
- Tested manually.

| 1 | 2 | 3 | 4 |
|--------|--------|--------|--------|
| <img width="804" alt="image"
src="https://github.com/user-attachments/assets/6a232089-8886-46a9-a30a-5df36720dca1">
| <img width="618" alt="image"
src="https://github.com/user-attachments/assets/dabb0703-e22c-413c-952e-eb0908a48645">
| <img width="624" alt="image"
src="https://github.com/user-attachments/assets/f45f7a6a-d7c9-4c14-bace-c6d8b980cf0a">
| <img width="782" alt="image"
src="https://github.com/user-attachments/assets/d7fd6dff-95fe-48c7-b030-a18602d1d516">
|

# Todos

- [ ] Add entry to changelog (if necessary).
Not necessary.
  • Loading branch information
mstrasinskis authored Sep 4, 2024
1 parent 32403a6 commit b9701e5
Show file tree
Hide file tree
Showing 14 changed files with 484 additions and 48 deletions.
2 changes: 1 addition & 1 deletion frontend/src/lib/components/accounts/IcrcWalletPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import type { Universe } from "$lib/types/universe";
import { selectableUniversesStore } from "$lib/derived/selectable-universes.derived";
export let testId: string;
export let testId: string = "icrc-wallet-page";
export let accountIdentifier: string | undefined | null = undefined;
export let ledgerCanisterId: Principal | undefined;
export let indexCanisterId: Principal | undefined;
Expand Down
23 changes: 18 additions & 5 deletions frontend/src/lib/components/accounts/ImportTokenForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
export let ledgerCanisterId: Principal | undefined = undefined;
export let indexCanisterId: Principal | undefined = undefined;
export let addIndexCanisterMode: boolean = false;
const dispatch = createEventDispatcher();
let isSubmitDisabled = true;
$: isSubmitDisabled = isNullish(ledgerCanisterId);
$: isSubmitDisabled = addIndexCanisterMode
? isNullish(indexCanisterId)
: isNullish(ledgerCanisterId);
</script>

<TestIdWrapper testId="import-token-form-component">
Expand All @@ -26,6 +29,7 @@
placeholderLabelKey="import_token.placeholder"
name="ledger-canister-id"
testId="ledger-canister-id"
disabled={addIndexCanisterMode}
>
<svelte:fragment slot="label"
>{$i18n.import_token.ledger_label}</svelte:fragment
Expand All @@ -34,19 +38,26 @@

<PrincipalInput
bind:principal={indexCanisterId}
required={false}
required={addIndexCanisterMode}
placeholderLabelKey="import_token.placeholder"
name="index-canister-id"
testId="index-canister-id"
>
<Html slot="label" text={$i18n.import_token.index_label_optional} />
<Html
slot="label"
text={addIndexCanisterMode
? $i18n.import_token.index_label
: $i18n.import_token.index_label_optional}
/>
</PrincipalInput>

<p class="description">
<Html text={$i18n.import_token.index_canister_description} />
</p>

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

<div class="toolbar">
<button
Expand All @@ -64,7 +75,9 @@
type="submit"
disabled={isSubmitDisabled}
>
{$i18n.core.next}
{addIndexCanisterMode
? $i18n.import_token.add_index_canister
: $i18n.core.next}
</button>
</div>
</form>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/components/ui/PrincipalInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export let principal: Principal | undefined = undefined;
export let required: boolean | undefined = undefined;
export let testId: string | undefined = undefined;
export let disabled: boolean | undefined = undefined;
let address = principal?.toText() ?? "";
$: principal = getPrincipalFromString(address);
Expand All @@ -26,6 +27,7 @@
{placeholderLabelKey}
{name}
{testId}
{disabled}
bind:value={address}
errorMessage={showError ? $i18n.error.principal_not_valid : undefined}
on:blur={showErrorIfAny}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@
"verifying": "Verifying token details...",
"importing": "Importing new token...",
"removing": "Removing imported token...",
"updating": "Updating imported token...",
"description": "To import a new token to your NNS dapp wallet, you will need to find, and paste the ledger canister id of the token. If you want to see your transaction history, you need to import the token’s index canister.",
"ledger_label": "Ledger Canister ID",
"index_label_optional": "Index Canister ID <span class='description'>(Optional)</span>",
Expand All @@ -1057,6 +1058,8 @@
"index_label": "Index Canister ID",
"index_fallback_label": "Transaction history won’t be displayed.",
"import_button": "Import",
"link_to_dashboard": "https://dashboard.internetcomputer.org/canister/$canisterId"
"link_to_dashboard": "https://dashboard.internetcomputer.org/canister/$canisterId",
"add_index_canister": "Add index canister",
"add_index_description": "Transaction history is not available. To see history add an index canister. <strong>Note:</strong> not all tokens have index canisters."
}
}
69 changes: 69 additions & 0 deletions frontend/src/lib/modals/accounts/AddIndexCanisterModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script lang="ts">
import ImportTokenForm from "$lib/components/accounts/ImportTokenForm.svelte";
import { matchLedgerIndexPair } from "$lib/services/icrc-index.services";
import { startBusy, stopBusy } from "$lib/stores/busy.store";
import { i18n } from "$lib/stores/i18n";
import { importedTokensStore } from "$lib/stores/imported-tokens.store";
import { Modal } from "@dfinity/gix-components";
import type { Principal } from "@dfinity/principal";
import { isNullish } from "@dfinity/utils";
import { createEventDispatcher } from "svelte";
import { addIndexCanister } from "../../services/imported-tokens.services";
export let ledgerCanisterId: Principal;
const dispatch = createEventDispatcher();
let indexCanisterId: Principal | undefined;
const nnsSubmit = async () => {
// Just for type safety. This should never happen.
if (
isNullish(ledgerCanisterId) ||
isNullish(indexCanisterId) ||
isNullish($importedTokensStore.importedTokens)
) {
return;
}
try {
startBusy({
initiator: "import-token-updating",
labelKey: "import_token.updating",
});
if (
!(await matchLedgerIndexPair({
ledgerCanisterId,
indexCanisterId,
}))
) {
return;
}
const { success } = await addIndexCanister({
ledgerCanisterId,
indexCanisterId,
importedTokens: $importedTokensStore.importedTokens,
});
if (success) {
dispatch("nnsClose");
}
} finally {
stopBusy("import-token-updating");
}
};
</script>

<Modal testId="add-index-canister-modal-component" on:nnsClose>
<svelte:fragment slot="title"
>{$i18n.import_token.add_index_canister}</svelte:fragment
>
<ImportTokenForm
addIndexCanisterMode
bind:ledgerCanisterId
bind:indexCanisterId
on:nnsClose
on:nnsSubmit={nnsSubmit}
/>
</Modal>
139 changes: 101 additions & 38 deletions frontend/src/lib/pages/IcrcWallet.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import IcrcWalletPage from "$lib/components/accounts/IcrcWalletPage.svelte";
import IcrcWalletTransactionsList from "$lib/components/accounts/IcrcWalletTransactionsList.svelte";
import NoTransactions from "$lib/components/accounts/NoTransactions.svelte";
import { icrcCanistersStore } from "$lib/derived/icrc-canisters.derived";
import { selectedIcrcTokenUniverseIdStore } from "$lib/derived/selected-universe.derived";
import { tokensByUniverseIdStore } from "$lib/derived/tokens.derived";
import { icrcCanistersStore } from "$lib/derived/icrc-canisters.derived";
import { importedTokensStore } from "$lib/stores/imported-tokens.store";
import type { CanisterId } from "$lib/types/canister";
import type { IcrcTokenMetadata } from "$lib/types/icrc";
import type { WalletStore } from "$lib/types/wallet.context";
import { nonNullish } from "@dfinity/utils";
import { isImportedToken as checkImportedToken } from "$lib/utils/imported-tokens.utils";
import { Html, IconCanistersPage, IconPlus } from "@dfinity/gix-components";
import { isNullish, nonNullish } from "@dfinity/utils";
import { writable } from "svelte/store";
import AddIndexCanisterModal from "$lib/modals/accounts/AddIndexCanisterModal.svelte";
import { i18n } from "$lib/stores/i18n";
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
export let accountIdentifier: string | undefined | null = undefined;
Expand All @@ -35,41 +41,98 @@
const reloadAccount = async () => await wallet.reloadAccount?.();
const reloadTransactions = () => transactions?.reloadTransactions?.();
let isImportedToken = false;
$: isImportedToken = checkImportedToken({
ledgerCanisterId: $selectedIcrcTokenUniverseIdStore,
importedTokens: $importedTokensStore.importedTokens,
});
let showAddIndexCanisterModal = false;
</script>

<IcrcWalletPage
testId="icrc-wallet-component"
{accountIdentifier}
{token}
ledgerCanisterId={$selectedIcrcTokenUniverseIdStore}
{indexCanisterId}
{selectedAccountStore}
bind:this={wallet}
{reloadTransactions}
>
<svelte:fragment slot="page-content">
{#if nonNullish($selectedAccountStore.account) && nonNullish($selectedIcrcTokenUniverseIdStore) && nonNullish(indexCanisterId)}
<IcrcWalletTransactionsList
account={$selectedAccountStore.account}
{indexCanisterId}
ledgerCanisterId={$selectedIcrcTokenUniverseIdStore}
{token}
bind:this={transactions}
/>
{:else}
<NoTransactions />
{/if}
</svelte:fragment>

<svelte:fragment slot="footer-actions">
{#if nonNullish($selectedAccountStore.account) && nonNullish(token) && nonNullish($selectedIcrcTokenUniverseIdStore)}
<IcrcTokenWalletFooter
ledgerCanisterId={$selectedIcrcTokenUniverseIdStore}
account={$selectedAccountStore.account}
{token}
{reloadAccount}
{reloadTransactions}
/>
{/if}
</svelte:fragment>
</IcrcWalletPage>
<TestIdWrapper testId="icrc-wallet-component">
<IcrcWalletPage
{accountIdentifier}
{token}
ledgerCanisterId={$selectedIcrcTokenUniverseIdStore}
{indexCanisterId}
{selectedAccountStore}
bind:this={wallet}
{reloadTransactions}
>
<svelte:fragment slot="page-content">
{#if isImportedToken && isNullish(indexCanisterId)}
<div class="no-index-canister">
<div class="icon">
<IconCanistersPage />
</div>
<p class="description">
<Html text={$i18n.import_token.add_index_description} />
</p>
<button
data-tid="add-index-canister-button"
class="ghost with-icon add-index-canister-button"
on:click={() => (showAddIndexCanisterModal = true)}
>
<IconPlus />{$i18n.import_token.add_index_canister}
</button>
</div>
{:else if isNullish($selectedAccountStore.account) || isNullish($selectedIcrcTokenUniverseIdStore) || isNullish(indexCanisterId)}
<NoTransactions />
{:else}
<IcrcWalletTransactionsList
account={$selectedAccountStore.account}
{indexCanisterId}
ledgerCanisterId={$selectedIcrcTokenUniverseIdStore}
{token}
bind:this={transactions}
/>
{/if}
</svelte:fragment>

<svelte:fragment slot="footer-actions">
{#if nonNullish($selectedAccountStore.account) && nonNullish(token) && nonNullish($selectedIcrcTokenUniverseIdStore)}
<IcrcTokenWalletFooter
ledgerCanisterId={$selectedIcrcTokenUniverseIdStore}
account={$selectedAccountStore.account}
{token}
{reloadAccount}
{reloadTransactions}
/>
{/if}
</svelte:fragment>
</IcrcWalletPage>

{#if showAddIndexCanisterModal && nonNullish($selectedIcrcTokenUniverseIdStore)}
<AddIndexCanisterModal
on:nnsClose={() => (showAddIndexCanisterModal = false)}
ledgerCanisterId={$selectedIcrcTokenUniverseIdStore}
/>
{/if}
</TestIdWrapper>

<style lang="scss">
.no-index-canister {
padding-top: var(--padding-3x);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--padding-3x);
text-align: center;
.icon {
max-width: calc(var(--padding) * 18);
}
p {
max-width: calc(var(--padding) * 38);
}
}
.add-index-canister-button {
gap: var(--padding);
color: var(--primary);
}
</style>
3 changes: 2 additions & 1 deletion frontend/src/lib/stores/busy.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export type BusyStateInitiatorType =
| "reload-receive-account"
| "import-token-validation"
| "import-token-importing"
| "import-token-removing";
| "import-token-removing"
| "import-token-updating";

export interface BusyState {
initiator: BusyStateInitiatorType;
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,7 @@ interface I18nImport_token {
verifying: string;
importing: string;
removing: string;
updating: string;
description: string;
ledger_label: string;
index_label_optional: string;
Expand All @@ -1117,6 +1118,8 @@ interface I18nImport_token {
index_fallback_label: string;
import_button: string;
link_to_dashboard: string;
add_index_canister: string;
add_index_description: string;
}

interface I18nNeuron_state {
Expand Down
Loading

0 comments on commit b9701e5

Please sign in to comment.