diff --git a/apps/core/src/components/index.ts b/apps/core/src/components/index.ts
index 9f005714aa5..33fa79344a8 100644
--- a/apps/core/src/components/index.ts
+++ b/apps/core/src/components/index.ts
@@ -7,3 +7,4 @@ export * from './Inputs';
export * from './QR';
export * from './collapsible';
export * from './providers';
+export * from './stake';
diff --git a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx b/apps/core/src/components/stake/StakedCard.tsx
similarity index 70%
rename from apps/wallet/src/ui/app/staking/home/StakedCard.tsx
rename to apps/core/src/components/stake/StakedCard.tsx
index 64ef1a09757..dad5c6f091a 100644
--- a/apps/wallet/src/ui/app/staking/home/StakedCard.tsx
+++ b/apps/core/src/components/stake/StakedCard.tsx
@@ -2,22 +2,15 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
-import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '@iota/core';
-import { determineCountDownText } from '_src/ui/app/shared/countdown-timer';
-import {
- type ExtendedDelegatedStake,
- TimeUnit,
- useFormatCoin,
- useGetTimeBeforeEpochNumber,
- useTimeAgo,
- ImageIcon,
-} from '@iota/core';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { Card, CardImage, CardType, CardBody, CardAction, CardActionType } from '@iota/apps-ui-kit';
import { useMemo } from 'react';
-import { Link } from 'react-router-dom';
-
import { useIotaClientQuery } from '@iota/dapp-kit';
+import { ImageIcon } from '../icon';
+import { determineCountDownText, ExtendedDelegatedStake } from '../../utils';
+import { TimeUnit, useFormatCoin, useGetTimeBeforeEpochNumber, useTimeAgo } from '../../hooks';
+import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '../../constants';
+import React from 'react';
export enum StakeState {
WarmUp = 'WARM_UP',
@@ -35,21 +28,22 @@ const STATUS_COPY: { [key in StakeState]: string } = {
[StakeState.InActive]: 'Inactive',
};
-interface StakeCardProps {
+interface StakedCardProps {
extendedStake: ExtendedDelegatedStake;
currentEpoch: number;
inactiveValidator?: boolean;
+ onClick: () => void;
}
// For delegationsRequestEpoch n through n + 2, show Start Earning
// Show epoch number or date/time for n + 3 epochs
-export function StakeCard({
+export function StakedCard({
extendedStake,
currentEpoch,
inactiveValidator = false,
-}: StakeCardProps) {
- const { stakedIotaId, principal, stakeRequestEpoch, estimatedReward, validatorAddress } =
- extendedStake;
+ onClick,
+}: StakedCardProps) {
+ const { principal, stakeRequestEpoch, estimatedReward, validatorAddress } = extendedStake;
// TODO: Once two step withdraw is available, add cool down and withdraw now logic
// For cool down epoch, show Available to withdraw add rewards to principal
@@ -115,32 +109,20 @@ export function StakeCard({
};
return (
-
-
-
-
-
-
-
+
+
-
-
+
+
+
+
);
}
diff --git a/apps/core/src/components/stake/index.ts b/apps/core/src/components/stake/index.ts
new file mode 100644
index 00000000000..e61e23e24a0
--- /dev/null
+++ b/apps/core/src/components/stake/index.ts
@@ -0,0 +1,4 @@
+// Copyright (c) 2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+export * from './StakedCard';
diff --git a/apps/core/src/constants/staking.constants.ts b/apps/core/src/constants/staking.constants.ts
index 5c1a2ea2b7e..c93842c2408 100644
--- a/apps/core/src/constants/staking.constants.ts
+++ b/apps/core/src/constants/staking.constants.ts
@@ -9,3 +9,4 @@ export const DELEGATED_STAKES_QUERY_REFETCH_INTERVAL = 30_000;
export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2;
export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1;
+export const MIN_NUMBER_IOTA_TO_STAKE = 1;
diff --git a/apps/core/src/utils/determineCountDownText.ts b/apps/core/src/utils/determineCountDownText.ts
new file mode 100644
index 00000000000..437a00b1e7a
--- /dev/null
+++ b/apps/core/src/utils/determineCountDownText.ts
@@ -0,0 +1,16 @@
+// Copyright (c) Mysten Labs, Inc.
+// Modifications Copyright (c) 2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+export function determineCountDownText({
+ timeAgo,
+ label,
+ endLabel,
+}: {
+ timeAgo: string;
+ label?: string;
+ endLabel?: string;
+}): string {
+ const showLabel = timeAgo !== endLabel;
+ return showLabel ? `${label} ${timeAgo}` : timeAgo;
+}
diff --git a/apps/core/src/utils/index.ts b/apps/core/src/utils/index.ts
index 9973f811e10..cfbe4e93729 100644
--- a/apps/core/src/utils/index.ts
+++ b/apps/core/src/utils/index.ts
@@ -21,6 +21,7 @@ export * from './api-env';
export * from './getExplorerPaths';
export * from './getExplorerLink';
export * from './truncateString';
+export * from './determineCountDownText';
export * from './stake';
export * from './transaction';
diff --git a/apps/ui-kit/src/lib/components/molecules/card/Card.tsx b/apps/ui-kit/src/lib/components/molecules/card/Card.tsx
index 9c25e58d84b..1870f2e77c9 100644
--- a/apps/ui-kit/src/lib/components/molecules/card/Card.tsx
+++ b/apps/ui-kit/src/lib/components/molecules/card/Card.tsx
@@ -32,6 +32,10 @@ export interface CardProps {
* Use case: When the card is wrapped with a Link component
*/
isHoverable?: boolean;
+ /**
+ * The 'data-testid' attribute value (used in e2e tests)
+ */
+ testId?: string;
}
export function Card({
@@ -40,6 +44,7 @@ export function Card({
isHoverable,
onClick,
children,
+ testId,
}: CardProps) {
return (
{children}
diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx
index df257d13625..c26c1a39411 100644
--- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx
+++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx
@@ -3,23 +3,43 @@
'use client';
-import { AmountBox, Box, StakeCard, StakeDialog, Button, useStakeDialog } from '@/components';
+import { StartStaking } from '@/components/staking-overview/StartStaking';
+import {
+ Button,
+ ButtonSize,
+ ButtonType,
+ DisplayStats,
+ InfoBox,
+ InfoBoxStyle,
+ InfoBoxType,
+ Panel,
+ Title,
+ TitleSize,
+} from '@iota/apps-ui-kit';
+import { StakeDialog } from '@/components';
+import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog';
import {
ExtendedDelegatedStake,
formatDelegatedStake,
- useFormatCoin,
useGetDelegatedStake,
useTotalDelegatedRewards,
useTotalDelegatedStake,
DELEGATED_STAKES_QUERY_REFETCH_INTERVAL,
DELEGATED_STAKES_QUERY_STALE_TIME,
+ StakedCard,
+ useFormatCoin,
} from '@iota/core';
-import { useCurrentAccount } from '@iota/dapp-kit';
+import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit';
+import { IotaSystemStateSummary } from '@iota/iota-sdk/client';
+import { Info } from '@iota/ui-icons';
+import { useMemo } from 'react';
+import { useStakeDialog } from '@/components/Dialogs/Staking/hooks/useStakeDialog';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
-import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog';
function StakingDashboardPage(): JSX.Element {
const account = useCurrentAccount();
+ const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
+ const activeValidators = (system as IotaSystemStateSummary)?.activeValidators;
const {
isDialogStakeOpen,
@@ -42,13 +62,29 @@ function StakingDashboardPage(): JSX.Element {
const extendedStakes = delegatedStakeData ? formatDelegatedStake(delegatedStakeData) : [];
const totalDelegatedStake = useTotalDelegatedStake(extendedStakes);
const totalDelegatedRewards = useTotalDelegatedRewards(extendedStakes);
- const [formattedDelegatedStake, stakeSymbol, stakeResult] = useFormatCoin(
+ const [totalDelegatedStakeFormatted, symbol] = useFormatCoin(
totalDelegatedStake,
IOTA_TYPE_ARG,
);
- const [formattedDelegatedRewards, rewardsSymbol, rewardsResult] = useFormatCoin(
- totalDelegatedRewards,
- IOTA_TYPE_ARG,
+ const [totalDelegatedRewardsFormatted] = useFormatCoin(totalDelegatedRewards, IOTA_TYPE_ARG);
+
+ const delegations = useMemo(() => {
+ return delegatedStakeData?.flatMap((delegation) => {
+ return delegation.stakes.map((d) => ({
+ ...d,
+ // flag any inactive validator for the stakeIota object
+ // if the stakingPoolId is not found in the activeValidators list flag as inactive
+ inactiveValidator: !activeValidators?.find(
+ ({ stakingPoolId }) => stakingPoolId === delegation.stakingPool,
+ ),
+ validatorAddress: delegation.validatorAddress,
+ }));
+ });
+ }, [activeValidators, delegatedStakeData]);
+
+ // Check if there are any inactive validators
+ const hasInactiveValidatorDelegation = delegations?.some(
+ ({ inactiveValidator }) => inactiveValidator,
);
const viewStakeDetails = (extendedStake: ExtendedDelegatedStake) => {
@@ -57,46 +93,93 @@ function StakingDashboardPage(): JSX.Element {
};
return (
- <>
-
-
-
-
-
-
List of stakes
- {extendedStakes?.map((extendedStake) => (
-
- ))}
+
+
+ {(delegatedStakeData?.length ?? 0) > 0 ? (
+
+ handleNewStake()}
+ size={ButtonSize.Small}
+ type={ButtonType.Primary}
+ text="Stake"
+ />
+ }
+ />
+
+
+
+
+
+
+
+ {hasInactiveValidatorDelegation ? (
+
+ }
+ style={InfoBoxStyle.Elevated}
+ />
+
+ ) : null}
+
+ {system &&
+ delegations
+ ?.filter(({ inactiveValidator }) => inactiveValidator)
+ .map((delegation) => (
+ viewStakeDetails(delegation)}
+ />
+ ))}
+
+
+ {system &&
+ delegations
+ ?.filter(({ inactiveValidator }) => !inactiveValidator)
+ .map((delegation) => (
+ viewStakeDetails(delegation)}
+ />
+ ))}
+
+
+
+
+
+ ) : (
+
+
-
-
+ )}
- {isDialogStakeOpen && stakeDialogView && (
-
- )}
- >
+
);
}
diff --git a/apps/wallet-dashboard/components/Cards/StakeCard.tsx b/apps/wallet-dashboard/components/Cards/StakeCard.tsx
deleted file mode 100644
index 9e79abd31ad..00000000000
--- a/apps/wallet-dashboard/components/Cards/StakeCard.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2024 IOTA Stiftung
-// SPDX-License-Identifier: Apache-2.0
-
-import React from 'react';
-import { Box, Button } from '@/components/index';
-import { ExtendedDelegatedStake } from '@iota/core';
-
-interface StakeCardProps {
- extendedStake: ExtendedDelegatedStake;
- onDetailsClick: (extendedStake: ExtendedDelegatedStake) => void;
-}
-
-function StakeCard({ extendedStake, onDetailsClick }: StakeCardProps): JSX.Element {
- return (
-
- Validator: {extendedStake.validatorAddress}
- Stake: {extendedStake.principal}
- {extendedStake.status === 'Active' && (
- Estimated reward: {extendedStake.estimatedReward}
- )}
- Status: {extendedStake.status}
-
-
- );
-}
-
-export default StakeCard;
diff --git a/apps/wallet-dashboard/components/Cards/index.ts b/apps/wallet-dashboard/components/Cards/index.ts
index da687047e80..a6bf6b83bcd 100644
--- a/apps/wallet-dashboard/components/Cards/index.ts
+++ b/apps/wallet-dashboard/components/Cards/index.ts
@@ -1,5 +1,4 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
-export { default as StakeCard } from './StakeCard';
export * from './VisualAssetDetailsCard';
diff --git a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx
index 6007798119b..bdd129698dd 100644
--- a/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx
+++ b/apps/wallet-dashboard/components/Dialogs/Staking/StakeDialog.tsx
@@ -18,6 +18,7 @@ import {
useGetValidatorsApy,
useBalance,
createValidationSchema,
+ MIN_NUMBER_IOTA_TO_STAKE,
} from '@iota/core';
import { FormikProvider, useFormik } from 'formik';
import type { FormikHelpers } from 'formik';
@@ -29,8 +30,6 @@ import { Dialog } from '@iota/apps-ui-kit';
import { DetailsView, UnstakeView } from './views';
import { FormValues } from './views/EnterAmountView';
-export const MIN_NUMBER_IOTA_TO_STAKE = 1;
-
export enum StakeDialogView {
Details = 'Details',
SelectValidator = 'SelectValidator',
@@ -43,14 +42,13 @@ const INITIAL_VALUES = {
};
interface StakeDialogProps {
- isTimelockedStaking?: boolean;
- onSuccess?: (digest: string) => void;
isOpen: boolean;
handleClose: () => void;
- view: StakeDialogView;
+ isTimelockedStaking?: boolean;
+ onSuccess?: (digest: string) => void;
+ view?: StakeDialogView;
setView?: (view: StakeDialogView) => void;
stakedDetails?: ExtendedDelegatedStake | null;
-
selectedValidator?: string;
setSelectedValidator?: (validator: string) => void;
}
diff --git a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx
index 21a4468fe30..746cba58e57 100644
--- a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx
+++ b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx
@@ -31,7 +31,7 @@ export function StakingData({ stakingData }: StakingDataProps) {
-
+
- {title}
-
-
-
- {formatted}
-
-
- {symbol}
-
-
-
- );
-}
diff --git a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx
index d86c202cb97..606c51ec947 100644
--- a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx
+++ b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx
@@ -10,12 +10,12 @@ import {
useTotalDelegatedStake,
DELEGATED_STAKES_QUERY_REFETCH_INTERVAL,
DELEGATED_STAKES_QUERY_STALE_TIME,
+ useFormatCoin,
+ StakedCard,
} from '@iota/core';
import { useIotaClientQuery } from '@iota/dapp-kit';
import { useMemo } from 'react';
import { useActiveAddress } from '../../hooks/useActiveAddress';
-import { StakeCard } from '../home/StakedCard';
-import { StatsDetail } from '_app/staking/validators/StatsDetail';
import {
Title,
TitleSize,
@@ -25,9 +25,11 @@ import {
InfoBoxStyle,
InfoBoxType,
LoadingIndicator,
+ DisplayStats,
} from '@iota/apps-ui-kit';
import { useNavigate } from 'react-router-dom';
import { Info, Warning } from '@iota/ui-icons';
+import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
export function ValidatorsCard() {
const accountAddress = useActiveAddress();
@@ -50,6 +52,11 @@ export function ValidatorsCard() {
// Total active stake for all Staked validators
const totalDelegatedStake = useTotalDelegatedStake(delegatedStake);
+ const [totalDelegatedStakeFormatted, symbol] = useFormatCoin(
+ totalDelegatedStake,
+ IOTA_TYPE_ARG,
+ );
+
const delegations = useMemo(() => {
return delegatedStakeData?.flatMap((delegation) => {
return delegation.stakes.map((d) => ({
@@ -72,6 +79,7 @@ export function ValidatorsCard() {
// Get total rewards for all delegations
const delegatedStakes = delegatedStakeData ? formatDelegatedStake(delegatedStakeData) : [];
const totalDelegatedRewards = useTotalDelegatedRewards(delegatedStakes);
+ const [totalDelegatedRewardsFormatted] = useFormatCoin(totalDelegatedRewards, IOTA_TYPE_ARG);
const handleNewStake = () => {
ampli.clickedStakeIota({
@@ -106,8 +114,16 @@ export function ValidatorsCard() {
return (
-
-
+
+
@@ -128,11 +144,19 @@ validator to start earning rewards again."
delegations
?.filter(({ inactiveValidator }) => inactiveValidator)
.map((delegation) => (
-
+ navigate(
+ `/stake/delegation-detail?${new URLSearchParams({
+ validator: delegation.validatorAddress,
+ staked: delegation.stakedIotaId,
+ }).toString()}`,
+ )
+ }
/>
))}
@@ -142,10 +166,18 @@ validator to start earning rewards again."
delegations
?.filter(({ inactiveValidator }) => !inactiveValidator)
.map((delegation) => (
-
+ navigate(
+ `/stake/delegation-detail?${new URLSearchParams({
+ validator: delegation.validatorAddress,
+ staked: delegation.stakedIotaId,
+ }).toString()}`,
+ )
+ }
/>
))}
diff --git a/apps/wallet/tests/staking.spec.ts b/apps/wallet/tests/staking.spec.ts
index de6fc021c0c..c984efef3ab 100644
--- a/apps/wallet/tests/staking.spec.ts
+++ b/apps/wallet/tests/staking.spec.ts
@@ -42,8 +42,8 @@ test('staking', async ({ page, extensionUrl }) => {
});
await page.getByText(`${STAKE_AMOUNT} IOTA`).click();
- await expect(page.getByTestId('stake-card')).toBeVisible({ timeout: LONG_TIMEOUT });
- await page.getByTestId('stake-card').click();
+ await expect(page.getByTestId('staked-card')).toBeVisible({ timeout: LONG_TIMEOUT });
+ await page.getByTestId('staked-card').click();
await page.getByText('Unstake').click();
await page.getByRole('button', { name: 'Unstake' }).click();