Skip to content

Commit

Permalink
Show stake in USD on SNS neuron details (#6044)
Browse files Browse the repository at this point in the history
# Motivation

Want to show the stake in USD on the neuron details page similar to how
we show the account balance on the wallet page.

#6039 did it for NNS neurons.
This PR does it for SNS neurons.
And a third PR will make sure token prices are loaded when the neuron
page is deep linked.

# Changes

1. Use `HeadingSubtitleWithUsdValue` in `SnsNeuronPageHeading`. Some of
the slot content is temporarily duplicate between with and without
feature flag.
2. Pass the `ledgerCanisterId` from `SnsNeuronDetail` to
`SnsNeuronPageHeading` as it's needed to find the token price.

# Tests

1. Unit tests added.
2. Manually at
https://qsgjb-riaaa-aaaaa-aaaga-cai.dskloet-ingress.devenv.dfinity.network/neuron/?u=bd3sg-teaaa-aaaaa-qaaba-cai&neuron=d09433a0369029e7b1114df00138c0cf5bbbef6d0d150c0aa6dd0fd67a75f985

# Todos

- [ ] Add entry to changelog (if necessary).
not yet
  • Loading branch information
dskloetd authored Dec 19, 2024
1 parent 8b7598b commit 0773802
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { selectedTokenStore } from "$lib/derived/selected-token.derived";
import { authStore } from "$lib/stores/auth.store";
import { ENABLE_USD_VALUES_FOR_NEURONS } from "$lib/stores/feature-flags.store";
import { i18n } from "$lib/stores/i18n";
import { replacePlaceholders } from "$lib/utils/i18n.utils";
import { formatVotingPower } from "$lib/utils/neuron.utils";
Expand All @@ -10,14 +11,17 @@
snsNeuronVotingPower,
} from "$lib/utils/sns-neuron.utils";
import HeadingSubtitle from "../common/HeadingSubtitle.svelte";
import HeadingSubtitleWithUsdValue from "../common/HeadingSubtitleWithUsdValue.svelte";
import PageHeading from "../common/PageHeading.svelte";
import AmountDisplay from "../ic/AmountDisplay.svelte";
import { Tag } from "@dfinity/gix-components";
import type { Principal } from "@dfinity/principal";
import type { SnsNervousSystemParameters, SnsNeuron } from "@dfinity/sns";
import { TokenAmountV2, nonNullish, type Token } from "@dfinity/utils";
export let neuron: SnsNeuron;
export let parameters: SnsNervousSystemParameters;
export let ledgerCanisterId: Principal | undefined;
let token: Token | undefined;
$: token = $selectedTokenStore;
Expand Down Expand Up @@ -47,15 +51,29 @@
<AmountDisplay {amount} size="huge" singleLine detailed />
{/if}
</svelte:fragment>
<HeadingSubtitle slot="subtitle" testId="voting-power">
{#if votingPower > 0}
{replacePlaceholders($i18n.neuron_detail.voting_power_subtitle, {
$votingPower: formatVotingPower(votingPower),
})}
<svelte:fragment slot="subtitle">
{#if $ENABLE_USD_VALUES_FOR_NEURONS}
<HeadingSubtitleWithUsdValue {amount} {ledgerCanisterId}>
{#if votingPower > 0}
{replacePlaceholders($i18n.neuron_detail.voting_power_subtitle, {
$votingPower: formatVotingPower(votingPower),
})}
{:else}
{$i18n.neuron_detail.voting_power_zero_subtitle}
{/if}
</HeadingSubtitleWithUsdValue>
{:else}
{$i18n.neuron_detail.voting_power_zero_subtitle}
<HeadingSubtitle testId="voting-power">
{#if votingPower > 0}
{replacePlaceholders($i18n.neuron_detail.voting_power_subtitle, {
$votingPower: formatVotingPower(votingPower),
})}
{:else}
{$i18n.neuron_detail.voting_power_zero_subtitle}
{/if}
</HeadingSubtitle>
{/if}
</HeadingSubtitle>
</svelte:fragment>
<svelte:fragment slot="tags">
{#if isHotkey}
<Tag size="large" testId="hotkey-tag">{$i18n.neurons.hotkey_control}</Tag>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lib/pages/SnsNeuronDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
let token: Token;
$: token = $snsTokenSymbolSelectedStore as Token;
let ledgerCanisterId: Principal | undefined;
$: ledgerCanisterId = $selectedUniverseStore.summary?.ledgerCanisterId;
let governanceCanisterId: Principal | undefined;
$: governanceCanisterId =
$selectedUniverseStore.summary?.governanceCanisterId;
Expand Down Expand Up @@ -193,6 +196,7 @@
<SnsNeuronPageHeading
{parameters}
neuron={$selectedSnsNeuronStore.neuron}
{ledgerCanisterId}
/>
<Separator spacing="none" />
<SnsNeuronVotingPowerSection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import SnsNeuronPageHeading from "$lib/components/sns-neuron-detail/SnsNeuronPageHeading.svelte";
import { CKUSDC_UNIVERSE_CANISTER_ID } from "$lib/constants/ckusdc-canister-ids.constants";
import {
SECONDS_IN_DAY,
SECONDS_IN_EIGHT_YEARS,
SECONDS_IN_FOUR_YEARS,
} from "$lib/constants/constants";
import { HOTKEY_PERMISSIONS } from "$lib/constants/sns-neurons.constants";
import { overrideFeatureFlagsStore } from "$lib/stores/feature-flags.store";
import { icpSwapTickersStore } from "$lib/stores/icp-swap.store";
import { nowInSeconds } from "$lib/utils/date.utils";
import { mockIdentity, resetIdentity } from "$tests/mocks/auth.store.mock";
import { mockIcpSwapTicker } from "$tests/mocks/icp-swap.mock";
import {
createMockSnsNeuron,
mockSnsNeuron,
snsNervousSystemParametersMock,
} from "$tests/mocks/sns-neurons.mock";
import { principal } from "$tests/mocks/sns-projects.mock";
import { SnsNeuronPageHeadingPo } from "$tests/page-objects/SnsNeuronPageHeading.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { NeuronState } from "@dfinity/nns";
Expand All @@ -28,6 +33,7 @@ describe("SnsNeuronPageHeading", () => {
const maxAgeForBonus = BigInt(SECONDS_IN_FOUR_YEARS);
const maxAgeBonusMultiplier = 1.25;
const maxAgeBonusPercentage = 100 * (maxAgeBonusMultiplier - 1);
const ledgerCanisterId = principal(555);

const renderSnsNeuronCmp = (neuron: SnsNeuron) => {
const { container } = render(SnsNeuronPageHeading, {
Expand All @@ -46,6 +52,7 @@ describe("SnsNeuronPageHeading", () => {
],
max_age_bonus_percentage: [BigInt(maxAgeBonusPercentage)],
},
ledgerCanisterId,
},
});

Expand All @@ -57,6 +64,19 @@ describe("SnsNeuronPageHeading", () => {
// Make sure that nowInSeconds() returns a fixed value for the calculation
// of the neuron age.
vi.useFakeTimers();

icpSwapTickersStore.set([
{
...mockIcpSwapTicker,
base_id: CKUSDC_UNIVERSE_CANISTER_ID.toText(),
last_price: "10.00",
},
{
...mockIcpSwapTicker,
base_id: ledgerCanisterId.toText(),
last_price: "100.00",
},
]);
});

it("should render the neuron's stake", async () => {
Expand Down Expand Up @@ -115,4 +135,29 @@ describe("SnsNeuronPageHeading", () => {

expect(await po.hasHotkeyTag()).toBe(true);
});

it("should not display USD balance if feature flag is disabled", async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_USD_VALUES_FOR_NEURONS", false);

const stake = 300_000_000n;
const neuron = createMockSnsNeuron({
stake,
});
const po = renderSnsNeuronCmp(neuron);

expect(await po.hasBalanceInUsd()).toBe(false);
});

it("should display USD balance if feature flag is enabled", async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_USD_VALUES_FOR_NEURONS", true);

const stake = 300_000_000n;
const neuron = createMockSnsNeuron({
stake,
});
const po = renderSnsNeuronCmp(neuron);

expect(await po.hasBalanceInUsd()).toBe(true);
expect(await po.getBalanceInUsd()).toBe("$0.30");
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AmountDisplayPo } from "$tests/page-objects/AmountDisplay.page-object";
import { BasePageObject } from "$tests/page-objects/base.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";
import { HeadingSubtitleWithUsdValuePo } from "./HeadingSubtitleWithUsdValue.page-object";

export class SnsNeuronPageHeadingPo extends BasePageObject {
private static readonly TID = "sns-neuron-page-heading-component";
Expand All @@ -11,6 +12,10 @@ export class SnsNeuronPageHeadingPo extends BasePageObject {
);
}

getHeadingSubtitleWithUsdValuePo(): HeadingSubtitleWithUsdValuePo {
return HeadingSubtitleWithUsdValuePo.under(this.root);
}

getAmountDisplayPo(): AmountDisplayPo {
return AmountDisplayPo.under(this.root);
}
Expand All @@ -26,4 +31,12 @@ export class SnsNeuronPageHeadingPo extends BasePageObject {
hasHotkeyTag(): Promise<boolean> {
return this.root.byTestId("hotkey-tag").isPresent();
}

hasBalanceInUsd(): Promise<boolean> {
return this.getHeadingSubtitleWithUsdValuePo().hasAmountInUsd();
}

getBalanceInUsd(): Promise<string> {
return this.getHeadingSubtitleWithUsdValuePo().getAmountInUsd();
}
}

0 comments on commit 0773802

Please sign in to comment.