Skip to content

Commit

Permalink
feat(staking): present on-chain data during staking management [LW-82…
Browse files Browse the repository at this point in the history
…67] (#523)

---------

Co-authored-by: Kamil Džurman <kamil.dzurman@gmail.com>
  • Loading branch information
przemyslaw-wlodek and xdzurman authored Sep 20, 2023
1 parent e6cdfc7 commit 25571ab
Show file tree
Hide file tree
Showing 12 changed files with 78 additions and 72 deletions.
40 changes: 13 additions & 27 deletions packages/staking/src/features/drawer/PoolDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card.Outlined className={PoolCard}>
Expand All @@ -36,25 +29,18 @@ export const PoolDetailsCard = ({ name, poolId, color }: PoolDetailsCardProps) =
<Box className={PoolIndicator} style={{ backgroundColor: color }} />
<Text.SubHeading>{name}</Text.SubHeading>
</Flex>
<Tooltip content={deleteEnabled ? undefined : t('drawer.preferences.pickMorePools')}>
<Tooltip content={onRemove ? undefined : t('drawer.preferences.pickMorePools')}>
<div>
<ControlButton.Icon
icon={<TrashIcon />}
onClick={handleRemovePoolFromPortfolio}
disabled={!deleteEnabled}
/>
<ControlButton.Icon icon={<TrashIcon />} onClick={onRemove} disabled={!onRemove} />
</div>
</Tooltip>
</Flex>
<Box className={PoolHr} />
<Flex justifyContent="space-between" alignItems="center">
<Text.Body.Normal weight="$semibold">
{t('drawer.preferences.percentageOfBalance', {
percentage: formatPercentages(1 / draftPortfolioLength, {
decimalPlaces: 0,
rounding: 'down',
}),
value: compactNumber(availableBalance / draftPortfolioLength),
{t('drawer.preferences.stakeValue', {
stakePercentage: weight,
stakeValue,
})}
</Text.Body.Normal>
</Flex>
Expand Down
20 changes: 15 additions & 5 deletions packages/staking/src/features/drawer/StakePoolPreferences.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -84,8 +88,14 @@ export const StakePoolPreferences = () => {
/>
</Flex>
<Flex flexDirection="column" gap="$16" pb="$32" alignItems="stretch">
{displayData.map(({ name, id, color }) => (
<PoolDetailsCard key={id} poolId={id} name={name} color={color} />
{displayData.map(({ name, id, color, weight }) => (
<PoolDetailsCard
key={id}
name={name}
color={color}
weight={weight}
onRemove={draftPortfolio.length > 1 ? createRemovePoolFromPortfolio(id) : undefined}
/>
))}
</Flex>
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion packages/staking/src/features/i18n/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/staking/src/features/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ type KeysStructure = {
preferences: {
selectedStakePools: '';
addPoolButton: '';
percentageOfBalance: '';
stakeValue: '';
pickMorePools: '';
nextButton: '';
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ 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"
/>
<hr />
<DelegationCard
arrangement="vertical"
balance="10000"
cardanoCoinSymbol="ADA"
distribution={[{ color: PieChartGradientColor.LaceLinearGradient, name: 'A', value: 1 }]}
distribution={[{ color: PieChartGradientColor.LaceLinearGradient, name: 'A', weight: 1 }]}
status="ready"
/>
</>
Expand Down
4 changes: 2 additions & 2 deletions packages/staking/src/features/overview/DelegationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type DelegationCardProps = {
cardanoCoinSymbol: string;
distribution: Array<{
name: string;
value: number;
weight: number;
color: PieChartColor;
}>;
status: DelegationStatus;
Expand Down Expand Up @@ -61,7 +61,7 @@ export const DelegationCard = ({
data-testid="delegation-info-card"
>
<div className={styles.chart} data-testid="delegation-chart">
<PieChart data={distribution} colors={distribution.map((d) => d.color)} />
<PieChart data={distribution} nameKey="name" valueKey="weight" />
{showDistribution && <Text.SubHeading className={styles.counter}>100%</Text.SubHeading>}
</div>
<div
Expand Down
6 changes: 1 addition & 5 deletions packages/staking/src/features/overview/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,7 @@ export const Overview = () => {
<DelegationCard
balance={compactNumber(balancesBalance.available.coinBalance)}
cardanoCoinSymbol={walletStoreWalletUICardanoCoin.symbol}
distribution={displayData.map(({ color, name = '-', weight }) => ({
color,
name,
value: weight,
}))}
distribution={displayData}
status={currentPortfolio.length === 1 ? 'simple-delegation' : 'multi-delegation'}
/>
</Box>
Expand Down
6 changes: 1 addition & 5 deletions packages/staking/src/features/overview/OverviewPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
/>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ exports[`delegationPortfolioStore > sets the current portfolio 1`] = `
},
"ticker": "8BOOL",
"value": 1n,
"weight": 33.33,
"weight": 33,
},
{
"displayData": {
Expand Down Expand Up @@ -76,7 +76,7 @@ exports[`delegationPortfolioStore > sets the current portfolio 1`] = `
},
"ticker": "8BOOM",
"value": 1n,
"weight": 33.33,
"weight": 33,
},
{
"displayData": {
Expand Down Expand Up @@ -114,7 +114,7 @@ exports[`delegationPortfolioStore > sets the current portfolio 1`] = `
},
"ticker": "ADACT",
"value": 1n,
"weight": 33.33,
"weight": 33,
},
]
`;
28 changes: 18 additions & 10 deletions packages/staking/src/features/store/delegationPortfolio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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,
}),
]);
});
Expand Down Expand Up @@ -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 },
]);
});
});
});
31 changes: 20 additions & 11 deletions packages/staking/src/features/store/delegationPortfolio.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<DraftPortfolioStakePool>((pool) => ({ ...pool, weight: Math.round(targetWeight / pools.length) }));

export const useDelegationPortfolioStore = create(
immer<DelegationPortfolioStore>((set, get) => ({
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 }),
Expand All @@ -85,7 +94,7 @@ export const useDelegationPortfolioStore = create(
stakePool,
ticker: stakePool.metadata?.ticker,
value: stake,
weight: percentage,
weight: Math.round(percentage * targetWeight),
};
});

Expand All @@ -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: {
Expand Down

0 comments on commit 25571ab

Please sign in to comment.