diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/form-dialog.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/form-dialog.tsx index 3c0fe196d9..0c5f9f6537 100644 --- a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/form-dialog.tsx +++ b/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/form-dialog.tsx @@ -1,5 +1,10 @@ import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { Dialog, DialogContent, DialogHeader } from '@penumbra-zone/ui/components/ui/dialog'; +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, +} from '@penumbra-zone/ui/components/ui/dialog'; import { IdentityKeyComponent } from '@penumbra-zone/ui/components/ui/identity-key-component'; import { InputBlock } from '../../../../shared/input-block'; import { Validator } from '@penumbra-zone/protobuf/penumbra/core/component/stake/v1/stake_pb'; @@ -9,10 +14,22 @@ import { getIdentityKey } from '@penumbra-zone/getters/validator'; import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view'; import { BalanceValueView } from '@penumbra-zone/ui/components/ui/balance-value-view'; import { NumberInput } from '../../../../shared/number-input'; +import { CircleAlert } from 'lucide-react'; +import { useStoreShallow } from '../../../../../utils/use-store-shallow.ts'; +import { AllSlices } from '../../../../../state'; +import { bech32mIdentityKey } from '@penumbra-zone/bech32m/penumbravalid'; const getCapitalizedAction = (action: 'delegate' | 'undelegate') => action.replace(/^./, firstCharacter => firstCharacter.toLocaleUpperCase()); +// If validator has > 5 voting power, show warning to user +const votingPowerSelector = + (validator: Validator, action?: 'delegate' | 'undelegate') => (state: AllSlices) => { + const votingPower = + state.staking.votingPowerByValidatorInfo[bech32mIdentityKey(getIdentityKey(validator))] ?? 0; + return action === 'delegate' && votingPower > 5; + }; + /** * Renders a dialog with a form for delegating to, or undelegating from, a * validator. @@ -51,6 +68,8 @@ export const FormDialog = ({ onClose: () => void; onSubmit: () => void; }) => { + const showDelegationWarning = useStoreShallow(votingPowerSelector(validator, action)); + const handleOpenChange = (open: boolean) => { if (!open) { onClose(); @@ -87,8 +106,8 @@ export const FormDialog = ({ {/** @todo: Refactor this block to use `InputToken` (with a new - boolean `showSelectModal` prop) once asset balances are - refactored as `ValueView`s. */} + boolean `showSelectModal` prop) once asset balances are + refactored as `ValueView`s. */} - - + {showDelegationWarning ? ( + + ) : ( + + )} )} @@ -120,3 +142,32 @@ export const FormDialog = ({ ); }; + +const DelegationVotingPowerWarning = ({ amount }: { amount: string }) => { + return ( + <> +
+ +
+ The validator you’re delegating to has more than 5% of the current voting power. To + promote decentralization, it’s recommended to choose a smaller validator. +
+
+
+ + + + +
+ + ); +}; diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.test.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.test.tsx index 001cb9683e..c5aacfbc64 100644 --- a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.test.tsx +++ b/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.test.tsx @@ -4,6 +4,7 @@ import { render } from '@testing-library/react'; import { ValidatorInfo } from '@penumbra-zone/protobuf/penumbra/core/component/stake/v1/stake_pb'; import { ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { AllSlices } from '../../../../../state'; +import { IdentityKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; const nonZeroBalance = new ValueView({ valueView: { @@ -23,7 +24,16 @@ const zeroBalance = new ValueView({ }, }); -const validatorInfo = new ValidatorInfo({ validator: {} }); +const validatorInfo = new ValidatorInfo({ + validator: { + identityKey: new IdentityKey({ + ik: new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, + 5, + ]), + }), + }, +}); let MOCK_STAKING_TOKENS_AND_FILTER: { stakingTokens: ValueView | undefined; @@ -44,7 +54,7 @@ type RecursivePartial = { vi.mock('../../../../../utils/use-store-shallow', async () => ({ ...(await vi.importActual('../../../../../utils/use-store-shallow')), useStoreShallow: (selector: (state: RecursivePartial) => unknown) => - selector({ staking: { account: 0 } }), + selector({ staking: { account: 0, votingPowerByValidatorInfo: { '': 0 } } }), })); describe('', () => {