From 25571abe72a7edc1dd304c31243a8a86bc85ef8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20W=C5=82odek?= Date: Wed, 20 Sep 2023 15:25:37 +0200 Subject: [PATCH] feat(staking): present on-chain data during staking management [LW-8267] (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Kamil Džurman --- .../src/features/drawer/PoolDetailsCard.tsx | 40 ++++++------------- .../features/drawer/StakePoolPreferences.tsx | 20 +++++++--- .../src/features/i18n/translations/en.ts | 2 +- packages/staking/src/features/i18n/types.ts | 2 +- .../overview/DelegationCard.stories.tsx | 4 +- .../src/features/overview/DelegationCard.tsx | 4 +- .../src/features/overview/Overview.tsx | 6 +-- .../src/features/overview/OverviewPopup.tsx | 6 +-- .../helpers/mapPortfolioToDisplayData.ts | 1 + .../delegationPortfolio.test.ts.snap | 6 +-- .../store/delegationPortfolio.test.ts | 28 ++++++++----- .../src/features/store/delegationPortfolio.ts | 31 +++++++++----- 12 files changed, 78 insertions(+), 72 deletions(-) diff --git a/packages/staking/src/features/drawer/PoolDetailsCard.tsx b/packages/staking/src/features/drawer/PoolDetailsCard.tsx index a944ecec2e..398cf55c35 100644 --- a/packages/staking/src/features/drawer/PoolDetailsCard.tsx +++ b/packages/staking/src/features/drawer/PoolDetailsCard.tsx @@ -1,32 +1,25 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import { Cardano } from '@cardano-sdk/core'; -import { formatPercentages } from '@lace/common'; import { Box, Card, ControlButton, Flex, PieChartColor, Text } from '@lace/ui'; import { useTranslation } from 'react-i18next'; import { useOutsideHandles } from '../outside-handles-provider'; import { Tooltip } from '../overview/staking-info-card/StatsTooltip'; -import { useDelegationPortfolioStore } from '../store'; import { PoolCard, PoolHr, PoolIndicator } from './StakePoolPreferences.css'; import TrashIcon from './trash.svg'; interface PoolDetailsCardProps { - poolId: Cardano.Cip17Pool['id']; name: string; color: PieChartColor; + weight: number; + onRemove?: () => void; } -export const PoolDetailsCard = ({ name, poolId, color }: PoolDetailsCardProps) => { +export const PoolDetailsCard = ({ name, color, weight, onRemove }: PoolDetailsCardProps) => { const { t } = useTranslation(); - const { draftPortfolioLength, portfolioMutators } = useDelegationPortfolioStore((state) => ({ - draftPortfolioLength: state.draftPortfolio.length, - portfolioMutators: state.mutators, - })); - const { balancesBalance, compactNumber } = useOutsideHandles(); - const availableBalance = Number(balancesBalance?.available?.coinBalance || '0'); - const handleRemovePoolFromPortfolio = () => { - portfolioMutators.removePoolInManagementProcess({ id: poolId }); - }; - const deleteEnabled = draftPortfolioLength > 1; + const { compactNumber, balancesBalance } = useOutsideHandles(); + const stakeValue = balancesBalance + ? // eslint-disable-next-line no-magic-numbers + compactNumber((weight / 100) * Number(balancesBalance.available.coinBalance)) + : '-'; return ( @@ -36,25 +29,18 @@ export const PoolDetailsCard = ({ name, poolId, color }: PoolDetailsCardProps) = {name} - +
- } - onClick={handleRemovePoolFromPortfolio} - disabled={!deleteEnabled} - /> + } onClick={onRemove} disabled={!onRemove} />
- {t('drawer.preferences.percentageOfBalance', { - percentage: formatPercentages(1 / draftPortfolioLength, { - decimalPlaces: 0, - rounding: 'down', - }), - value: compactNumber(availableBalance / draftPortfolioLength), + {t('drawer.preferences.stakeValue', { + stakePercentage: weight, + stakeValue, })} diff --git a/packages/staking/src/features/drawer/StakePoolPreferences.tsx b/packages/staking/src/features/drawer/StakePoolPreferences.tsx index 250b7b66c8..501eba8957 100644 --- a/packages/staking/src/features/drawer/StakePoolPreferences.tsx +++ b/packages/staking/src/features/drawer/StakePoolPreferences.tsx @@ -1,3 +1,4 @@ +import { Wallet } from '@lace/cardano'; import { Button, ControlButton, Flex, PIE_CHART_DEFAULT_COLOR_SET, PieChartColor, Text } from '@lace/ui'; import { useTranslation } from 'react-i18next'; import { useOutsideHandles } from '../outside-handles-provider'; @@ -47,12 +48,15 @@ export const StakePoolPreferences = () => { setIsDrawerVisible: store.setIsDrawerVisible, })); - const displayData = draftPortfolio.map(({ name, id }, i) => ({ + const displayData = draftPortfolio.map(({ name = '-', weight, id }, i) => ({ color: PIE_CHART_DEFAULT_COLOR_SET[i] as PieChartColor, id, - name: name || '', - value: 1, + name, + weight, })); + const createRemovePoolFromPortfolio = (poolId: Wallet.Cardano.PoolIdHex) => () => { + portfolioMutators.removePoolInManagementProcess({ id: poolId }); + }; const addPoolButtonDisabled = draftPortfolio.length === MAX_POOLS_COUNT; const onAddPoolButtonClick = () => { if (addPoolButtonDisabled) return; @@ -84,8 +88,14 @@ export const StakePoolPreferences = () => { /> - {displayData.map(({ name, id, color }) => ( - + {displayData.map(({ name, id, color, weight }) => ( + 1 ? createRemovePoolFromPortfolio(id) : undefined} + /> ))} diff --git a/packages/staking/src/features/i18n/translations/en.ts b/packages/staking/src/features/i18n/translations/en.ts index 28b3d6c184..4ea8f3eb48 100644 --- a/packages/staking/src/features/i18n/translations/en.ts +++ b/packages/staking/src/features/i18n/translations/en.ts @@ -58,9 +58,9 @@ export const en: Translations = { 'drawer.failure.title': 'Oops! Something went wrong...', 'drawer.preferences.addPoolButton': 'Add stake pool', 'drawer.preferences.nextButton': 'Next', - 'drawer.preferences.percentageOfBalance': '~{{value}} ADA (~{{percentage}}%)', 'drawer.preferences.pickMorePools': 'You need to stake at least to one pool.', 'drawer.preferences.selectedStakePools': 'Selected stake pools ({{count}})', + 'drawer.preferences.stakeValue': '~{{stakeValue}} ADA (~{{stakePercentage}}%)', 'drawer.sign.confirmation.title': 'Staking confirmation', 'drawer.sign.enterWalletPasswordToConfirmTransaction': 'Enter your wallet password to confirm transaction', 'drawer.sign.error.invalidPassword': 'Wrong password', diff --git a/packages/staking/src/features/i18n/types.ts b/packages/staking/src/features/i18n/types.ts index 9b079583a1..ff375ab449 100644 --- a/packages/staking/src/features/i18n/types.ts +++ b/packages/staking/src/features/i18n/types.ts @@ -123,7 +123,7 @@ type KeysStructure = { preferences: { selectedStakePools: ''; addPoolButton: ''; - percentageOfBalance: ''; + stakeValue: ''; pickMorePools: ''; nextButton: ''; }; diff --git a/packages/staking/src/features/overview/DelegationCard.stories.tsx b/packages/staking/src/features/overview/DelegationCard.stories.tsx index 8572df712e..bcadfa2c9f 100644 --- a/packages/staking/src/features/overview/DelegationCard.stories.tsx +++ b/packages/staking/src/features/overview/DelegationCard.stories.tsx @@ -8,7 +8,7 @@ export const DelegationCardStory: Story = () => ( arrangement="horizontal" balance="10000" cardanoCoinSymbol="ADA" - distribution={[{ color: PieChartGradientColor.LaceLinearGradient, name: 'A', value: 1 }]} + distribution={[{ color: PieChartGradientColor.LaceLinearGradient, name: 'A', weight: 1 }]} status="ready" />
@@ -16,7 +16,7 @@ export const DelegationCardStory: Story = () => ( arrangement="vertical" balance="10000" cardanoCoinSymbol="ADA" - distribution={[{ color: PieChartGradientColor.LaceLinearGradient, name: 'A', value: 1 }]} + distribution={[{ color: PieChartGradientColor.LaceLinearGradient, name: 'A', weight: 1 }]} status="ready" /> diff --git a/packages/staking/src/features/overview/DelegationCard.tsx b/packages/staking/src/features/overview/DelegationCard.tsx index 9bd3ec3c4b..d2caaa900c 100644 --- a/packages/staking/src/features/overview/DelegationCard.tsx +++ b/packages/staking/src/features/overview/DelegationCard.tsx @@ -13,7 +13,7 @@ type DelegationCardProps = { cardanoCoinSymbol: string; distribution: Array<{ name: string; - value: number; + weight: number; color: PieChartColor; }>; status: DelegationStatus; @@ -61,7 +61,7 @@ export const DelegationCard = ({ data-testid="delegation-info-card" >
- d.color)} /> + {showDistribution && 100%}
{ ({ - color, - name, - value: weight, - }))} + distribution={displayData} status={currentPortfolio.length === 1 ? 'simple-delegation' : 'multi-delegation'} /> diff --git a/packages/staking/src/features/overview/OverviewPopup.tsx b/packages/staking/src/features/overview/OverviewPopup.tsx index c99262ed57..09f6418e7c 100644 --- a/packages/staking/src/features/overview/OverviewPopup.tsx +++ b/packages/staking/src/features/overview/OverviewPopup.tsx @@ -86,11 +86,7 @@ export const OverviewPopup = () => { balance={compactNumber(balancesBalance.available.coinBalance)} cardanoCoinSymbol={walletStoreWalletUICardanoCoin.symbol} arrangement="vertical" - distribution={displayData.map(({ color, name = '-', weight }) => ({ - color, - name, - value: weight, - }))} + distribution={displayData} status={currentPortfolio.length === 1 ? 'simple-delegation' : 'multi-delegation'} /> diff --git a/packages/staking/src/features/overview/helpers/mapPortfolioToDisplayData.ts b/packages/staking/src/features/overview/helpers/mapPortfolioToDisplayData.ts index 4c4742d0ea..6fbceffa68 100644 --- a/packages/staking/src/features/overview/helpers/mapPortfolioToDisplayData.ts +++ b/packages/staking/src/features/overview/helpers/mapPortfolioToDisplayData.ts @@ -22,6 +22,7 @@ export const mapPortfolioToDisplayData = ({ color: PIE_CHART_DEFAULT_COLOR_SET[index] as PieChartColor, fiat: cardanoPrice, lastReward: Wallet.util.lovelacesToAdaString(item.displayData.lastReward.toString()), + name: item.displayData.name || '-', status: ((): 'retired' | 'saturated' | undefined => { if (item.displayData.retired) return 'retired'; if (Number(item.displayData.saturation || 0) > SATURATION_UPPER_BOUND) return 'saturated'; diff --git a/packages/staking/src/features/store/__snapshots__/delegationPortfolio.test.ts.snap b/packages/staking/src/features/store/__snapshots__/delegationPortfolio.test.ts.snap index 1722c1b0fe..cf5d67df06 100644 --- a/packages/staking/src/features/store/__snapshots__/delegationPortfolio.test.ts.snap +++ b/packages/staking/src/features/store/__snapshots__/delegationPortfolio.test.ts.snap @@ -38,7 +38,7 @@ exports[`delegationPortfolioStore > sets the current portfolio 1`] = ` }, "ticker": "8BOOL", "value": 1n, - "weight": 33.33, + "weight": 33, }, { "displayData": { @@ -76,7 +76,7 @@ exports[`delegationPortfolioStore > sets the current portfolio 1`] = ` }, "ticker": "8BOOM", "value": 1n, - "weight": 33.33, + "weight": 33, }, { "displayData": { @@ -114,7 +114,7 @@ exports[`delegationPortfolioStore > sets the current portfolio 1`] = ` }, "ticker": "ADACT", "value": 1n, - "weight": 33.33, + "weight": 33, }, ] `; diff --git a/packages/staking/src/features/store/delegationPortfolio.test.ts b/packages/staking/src/features/store/delegationPortfolio.test.ts index 84e8fc5fc1..ed8a3066dd 100644 --- a/packages/staking/src/features/store/delegationPortfolio.test.ts +++ b/packages/staking/src/features/store/delegationPortfolio.test.ts @@ -113,19 +113,19 @@ describe('delegationPortfolioStore', () => { currentEpoch: dummyCurrentEpoch, delegationDistribution: [ { - percentage: Wallet.Percent(33.33), + percentage: Wallet.Percent(0.33), pool: dummyStakePool1, rewardAccounts: [], stake: BigInt(1), }, { - percentage: Wallet.Percent(33.33), + percentage: Wallet.Percent(0.33), pool: dummyStakePool2, rewardAccounts: [], stake: BigInt(1), }, { - percentage: Wallet.Percent(33.33), + percentage: Wallet.Percent(0.33), pool: dummyStakePool3, rewardAccounts: [], stake: BigInt(1), @@ -243,9 +243,9 @@ describe('delegationPortfolioStore', () => { result.current.mutators.cancelManagementProcess({ dumpDraftToSelections: true }); }); expect(result.current.selections).toEqual([ - expect.objectContaining(dummyPool1), - expect.objectContaining(dummyPool2), - expect.objectContaining(dummyPool3), + expect.objectContaining({ ...dummyPool1, weight: 33 }), + expect.objectContaining({ ...dummyPool2, weight: 33 }), + expect.objectContaining({ ...dummyPool3, weight: 33 }), ]); }); it('re-sets the activeManagementProcess to none when finalized', () => { @@ -268,11 +268,11 @@ describe('delegationPortfolioStore', () => { expect(result.current.draftPortfolio).toEqual([ expect.objectContaining({ ...dummyPool2, - weight: 33.33, + weight: 50, }), expect.objectContaining({ ...dummyPool3, - weight: 33.33, + weight: 50, }), ]); }); @@ -334,12 +334,20 @@ describe('delegationPortfolioStore', () => { it('allows to remove pool from draftPortfolio', () => { const { result } = renderHook(() => useDelegationPortfolioStore()); act(() => result.current.mutators.removePoolInManagementProcess({ id: dummyPool3.id })); - expect(result.current.draftPortfolio).toEqual([dummyPool2, dummyPool4, dummyPool5]); + expect(result.current.draftPortfolio).toEqual([ + { ...dummyPool2, weight: 33 }, + { ...dummyPool4, weight: 33 }, + { ...dummyPool5, weight: 33 }, + ]); }); it('removes pool from selections when removed pool in management process', () => { const { result } = renderHook(() => useDelegationPortfolioStore()); act(() => result.current.mutators.removePoolInManagementProcess({ id: dummyPool1.id })); - expect(result.current.selections).toEqual([dummyPool2, dummyPool4, dummyPool5]); + expect(result.current.selections).toEqual([ + { ...dummyPool2, weight: 33 }, + { ...dummyPool4, weight: 33 }, + { ...dummyPool5, weight: 33 }, + ]); }); }); }); diff --git a/packages/staking/src/features/store/delegationPortfolio.ts b/packages/staking/src/features/store/delegationPortfolio.ts index cc760e9b0c..73c5384177 100644 --- a/packages/staking/src/features/store/delegationPortfolio.ts +++ b/packages/staking/src/features/store/delegationPortfolio.ts @@ -1,7 +1,12 @@ import { Wallet } from '@lace/cardano'; import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; -import { DelegationPortfolioState, DelegationPortfolioStore, PortfolioManagementProcess } from './types'; +import { + DelegationPortfolioState, + DelegationPortfolioStore, + DraftPortfolioStakePool, + PortfolioManagementProcess, +} from './types'; const defaultState: DelegationPortfolioState = { activeManagementProcess: PortfolioManagementProcess.None, @@ -12,6 +17,11 @@ const defaultState: DelegationPortfolioState = { export const MAX_POOLS_COUNT = 5; const LAST_STABLE_EPOCH = 2; +// interchangeable with percentages +const targetWeight = 100; + +const mapPoolWeights = (pools: DraftPortfolioStakePool[]) => + pools.map((pool) => ({ ...pool, weight: Math.round(targetWeight / pools.length) })); export const useDelegationPortfolioStore = create( immer((set, get) => ({ @@ -29,10 +39,7 @@ export const useDelegationPortfolioStore = create( set((store) => { if (store.activeManagementProcess === PortfolioManagementProcess.None) return; if (dumpDraftToSelections) { - store.selections = store.draftPortfolio.map((pool) => ({ - ...pool, - weight: 1, - })); + store.selections = mapPoolWeights(store.draftPortfolio); } store.draftPortfolio = []; store.activeManagementProcess = PortfolioManagementProcess.None; @@ -53,17 +60,18 @@ export const useDelegationPortfolioStore = create( removePoolInManagementProcess: ({ id }) => set((store) => { if (store.activeManagementProcess === PortfolioManagementProcess.None) return; - store.draftPortfolio = store.draftPortfolio.filter((pool) => pool.id !== id); + store.draftPortfolio = mapPoolWeights(store.draftPortfolio.filter((pool) => pool.id !== id)); if (store.activeManagementProcess === PortfolioManagementProcess.NewPortfolio) { store.selections = store.draftPortfolio; } }), selectPool: (poolData) => - set(({ selections }) => { + set((store) => { const { selectionsFull } = get().queries; - const alreadySelected = selections.some(({ id }) => poolData.id === id); + const alreadySelected = store.selections.some(({ id }) => poolData.id === id); if (selectionsFull() || alreadySelected) return; - selections.push(poolData); + store.selections.push(poolData); + store.selections = mapPoolWeights(store.selections); }), setCurrentPortfolio: async ({ cardanoCoin, delegationDistribution, delegationRewardsHistory, currentEpoch }) => { const lastNonVolatileEpoch = currentEpoch.epochNo.valueOf() - LAST_STABLE_EPOCH; @@ -74,6 +82,7 @@ export const useDelegationPortfolioStore = create( const confirmedPoolRewards = confirmedRewardHistory .filter(({ poolId }) => poolId === stakePool.id) .map(({ rewards }) => rewards); + return { displayData: { ...Wallet.util.stakePoolTransformer({ cardanoCoin, stakePool }), @@ -85,7 +94,7 @@ export const useDelegationPortfolioStore = create( stakePool, ticker: stakePool.metadata?.ticker, value: stake, - weight: percentage, + weight: Math.round(percentage * targetWeight), }; }); @@ -95,7 +104,7 @@ export const useDelegationPortfolioStore = create( }, unselectPool: ({ id }) => set((store) => { - store.selections = store.selections.filter((pool) => pool.id !== id); + store.selections = mapPoolWeights(store.selections.filter((pool) => pool.id !== id)); }), }, queries: {