Skip to content

Commit

Permalink
Merge pull request #382 from Concordium/p8/suspension-status
Browse files Browse the repository at this point in the history
P8/suspension status
  • Loading branch information
soerenbf authored Feb 10, 2025
2 parents 3a3a4c4 + 2a92229 commit 121997c
Show file tree
Hide file tree
Showing 16 changed files with 289 additions and 46 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Added

- Account validation/delegation overview now shows if the target pool is suspended or primed for suspension
- A notification is shown for accounts validating/delegating to a pool which is suspended

## 1.7.4

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { NotificationLevel } from '~/features/NotificationSlice';
import Loading from '~/cross-app-components/Loading';

import Notification from './Notification';
import Countdown from './Countdown';
import Countdown from '../Countdown';

enum UpdateStatus {
Available,
Expand Down
File renamed without changes.
70 changes: 70 additions & 0 deletions app/components/Notification/SuspendedValidatorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AccountInfoType } from '@concordium/web-sdk';
import { push } from 'connected-react-router';

import { NotificationLevel } from '~/features/NotificationSlice';
import Button from '~/cross-app-components/Button';
import { accountsInfoSelector, chooseAccount } from '~/features/AccountSlice';
import routes from '~/constants/routes.json';
import Notification from './Notification';
import { displaySplitAddress } from '~/utils/accountHelpers';

type Props = {
onClose(): void;
accountAddress: string;
};

/**
* A notification to be shown when an account is delegating to a staking pool
* that is going to close in the near future.
*/
export default function SuspendedValidatorNotification({
onClose,
accountAddress,
}: Props) {
const dispatch = useDispatch();
const accountInfo = useSelector(accountsInfoSelector)[accountAddress];

const toAccount = () => {
let route: string;
switch (accountInfo.type) {
case AccountInfoType.Baker: {
route = routes.ACCOUNTS_BAKING;
break;
}
case AccountInfoType.Delegator: {
route = routes.ACCOUNTS_DELEGATION;
break;
}
default: {
throw new Error('Expected account to be staking');
}
}

dispatch(chooseAccount(accountAddress));
dispatch(push(route));

onClose();
};

return (
<Notification
className="flexColumn alignCenter"
level={NotificationLevel.Error}
onCloseClick={onClose}
>
<div className="flex alignCenter">
The target pool of the following account is currently suspended.
</div>
<div className="flex alignCenter mT10 textBreakAll">
<b>{displaySplitAddress(accountAddress)}</b>
</div>
<div className="inlineFlexColumn mT10">
<Button className="mT10" size="tiny" onClick={toAccount}>
Go to account
</Button>
</div>
</Notification>
);
}
75 changes: 63 additions & 12 deletions app/features/AccountSlice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { LOCATION_CHANGE } from 'connected-react-router';
import { AccountInfoType, DelegationTargetType } from '@concordium/web-sdk';

import { getCredentialsOfAccount } from '~/database/CredentialDao';
import { hasPendingTransactions } from '~/database/TransactionDao';
import { accountSimpleView, defaultAccount } from '~/database/PreferencesDao';
import { stringify, parse } from '~/utils/JSONHelper';
import { getCredId } from '~/utils/credentialHelper';
import { throwLoggedError, mapRecordValues } from '~/utils/basicHelpers';
import {
getAccountInfo,
getAccountInfoOfCredential,
} from '~/node/nodeRequests';

// eslint-disable-next-line import/no-cycle
import { RootState } from '../store/store';
// eslint-disable-next-line import/no-cycle
Expand All @@ -18,7 +31,6 @@ import {
removeAccount as removeAccountFromDatabase,
findAccounts,
} from '../database/AccountDao';
import { getCredentialsOfAccount } from '~/database/CredentialDao';
import {
decryptAmounts,
getAddressFromCredentialId,
Expand All @@ -35,22 +47,15 @@ import {
TransactionFilter,
Hex,
IdentityVersion,
AccountExtras,
} from '../utils/types';
import { createAccount, isValidAddress } from '../utils/accountHelpers';
import {
getAccountInfoOfAddress,
getPoolStatusLatest,
getStatus,
getlastFinalizedBlockHash,
} from '../node/nodeHelpers';
import { hasPendingTransactions } from '~/database/TransactionDao';
import { accountSimpleView, defaultAccount } from '~/database/PreferencesDao';
import { stringify, parse } from '~/utils/JSONHelper';
import { getCredId } from '~/utils/credentialHelper';
import { throwLoggedError, mapRecordValues } from '~/utils/basicHelpers';
import {
getAccountInfo,
getAccountInfoOfCredential,
} from '~/node/nodeRequests';

export interface AccountState {
simpleView: boolean;
Expand All @@ -59,6 +64,7 @@ export interface AccountState {
chosenAccountAddress: string;
accountChanged: boolean;
defaultAccount: string | undefined;
accountExtras: Record<string, AccountExtras>;
}

type AccountByIndexTuple = [number, Account];
Expand Down Expand Up @@ -110,6 +116,7 @@ const initialState: AccountState = {
chosenAccountAddress: '',
accountChanged: true,
defaultAccount: undefined,
accountExtras: {},
};

const accountsSlice = createSlice({
Expand Down Expand Up @@ -147,6 +154,20 @@ const accountsSlice = createSlice({
setChosenAccountAddress(state, input.payload);
}
},
setSuspensionStatus(
state,
input: PayloadAction<{
address: string;
isSuspended: boolean;
}>
) {
if (state.accountExtras[input.payload.address] === undefined) {
state.accountExtras[input.payload.address] = {};
}

state.accountExtras[input.payload.address].isSuspended =
input.payload.isSuspended;
},
addToAccountInfos: (
state,
map: PayloadAction<Record<string, string>>
Expand Down Expand Up @@ -202,7 +223,10 @@ export const initialAccountNameSelector = (identityId: number) => (
)?.name;

export const accountsInfoSelector = (state: RootState) =>
mapRecordValues(state.accounts.accountsInfo, parse);
mapRecordValues<string, string, AccountInfo>(
state.accounts.accountsInfo,
parse
);

export const chosenAccountSelector = (state: RootState) =>
state.accounts.accounts.find(
Expand Down Expand Up @@ -234,7 +258,11 @@ export const {
previousConfirmedAccount,
} = accountsSlice.actions;

const { updateAccounts, updateAccountFields } = accountsSlice.actions;
const {
updateAccounts,
updateAccountFields,
setSuspensionStatus,
} = accountsSlice.actions;

function updateAccountInfoEntry(
dispatch: Dispatch,
Expand Down Expand Up @@ -427,6 +455,29 @@ async function updateAccountFromAccountInfo(
);
}

let validatorId: bigint | undefined;
if (accountInfo.type === AccountInfoType.Baker) {
validatorId = accountInfo.accountBaker.bakerId;
} else if (
accountInfo.type === AccountInfoType.Delegator &&
accountInfo.accountDelegation.delegationTarget.delegateType ===
DelegationTargetType.Baker
) {
validatorId = accountInfo.accountDelegation.delegationTarget.bakerId;
}

if (validatorId !== undefined) {
const poolStatus = await getPoolStatusLatest(validatorId);
if (poolStatus.isSuspended) {
dispatch(
setSuspensionStatus({
address: account.address,
isSuspended: true,
})
);
}
}

return updateCredentialsStatus(dispatch, account.address, accountInfo);
}

Expand Down
56 changes: 54 additions & 2 deletions app/features/NotificationSlice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { createSlice, Dispatch, PayloadAction } from '@reduxjs/toolkit';
import {
createSlice,
Dispatch,
Middleware,
PayloadAction,
} from '@reduxjs/toolkit';
import type { RootState } from '~/store/store';

export enum NotificationLevel {
Info = 'info',
Error = 'error',
ManualUpdate = 'manual-update',
AutoUpdate = 'auto-update',
ClosingBakerPool = 'closing-baker-pool',
SuspendedValidatorPool = 'suspended-validator-pool',
}

let nextId = 0;
Expand All @@ -24,12 +31,18 @@ export interface ClosingBakerNotification extends NotificationBase {
accountName: string;
}

export interface SuspendedValidatorNotification extends NotificationBase {
level: NotificationLevel.SuspendedValidatorPool;
accountAddress: string;
}

export interface Notification extends NotificationBase {
level: Exclude<
NotificationLevel,
| NotificationLevel.ManualUpdate
| NotificationLevel.AutoUpdate
| NotificationLevel.ClosingBakerPool
| NotificationLevel.SuspendedValidatorPool
>;
message: string;
}
Expand All @@ -39,6 +52,7 @@ interface NotificationSliceState {
| UpdateNotification
| Notification
| ClosingBakerNotification
| SuspendedValidatorNotification
)[];
}

Expand All @@ -51,7 +65,10 @@ const { actions, reducer } = createSlice({
pushNotification(
state,
action: PayloadAction<
Notification | UpdateNotification | ClosingBakerNotification
| Notification
| UpdateNotification
| ClosingBakerNotification
| SuspendedValidatorNotification
>
) {
state.notifications.push({ ...action.payload });
Expand Down Expand Up @@ -100,6 +117,20 @@ export function triggerClosingBakerPoolNotification(
nextId += 1;
}

export function triggerSuspendedValidatorPoolNotification(
dispatch: Dispatch,
accountAddress: string
) {
dispatch(
actions.pushNotification({
level: NotificationLevel.SuspendedValidatorPool,
id: nextId,
accountAddress,
})
);
nextId += 1;
}

/**
* Display notifications of different types.
*
Expand Down Expand Up @@ -127,3 +158,24 @@ export function pushNotification(

return close;
}

export const notificationsMiddleware: Middleware = (store) => (next) => (
action
) => {
const prevState: RootState = store.getState();
const result = next(action);
const nextState: RootState = store.getState();

Object.entries(nextState.accounts.accountExtras)
.filter(
([address, value]) =>
value.isSuspended !==
prevState.accounts.accountExtras[address]?.isSuspended &&
value.isSuspended
)
.forEach(([address]) => {
triggerSuspendedValidatorPoolNotification(store.dispatch, address);
});

return result;
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const Template: Story<{ account: Account; accountInfo: AccountInfo }> = ({
chosenAccountAddress: account.address,
accountChanged: true,
defaultAccount: account.address,
accountExtras: {},
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
AccountInfo,
AccountInfoDelegator,
isDelegatorAccount,
AccountInfoType,
} from '@concordium/web-sdk';

import React from 'react';
Expand Down Expand Up @@ -46,7 +46,7 @@ interface Props {
}

export default function Delegation({ account, accountInfo }: Props) {
const isDelegating = isDelegatorAccount(accountInfo);
const isDelegating = accountInfo.type === AccountInfoType.Delegator;
const { pathname } = useLocation();

if (!pathname.startsWith(routes.ACCOUNTS_ADD_DELEGATION) && !isDelegating) {
Expand Down
Loading

0 comments on commit 121997c

Please sign in to comment.