feat(governance): multisig warning (#3994)
This commit is contained in:
parent
3f01b93159
commit
6f9f432c90
@ -0,0 +1 @@
|
|||||||
|
export * from './multisig-incorrect-notice';
|
@ -0,0 +1,52 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
import { MultisigIncorrectNotice } from './multisig-incorrect-notice';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/web3', () => ({
|
||||||
|
useEthereumConfig: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/environment', () => ({
|
||||||
|
useEnvironment: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('MultisigIncorrectNotice', () => {
|
||||||
|
it('renders correctly when config is provided', () => {
|
||||||
|
(useEthereumConfig as jest.Mock).mockReturnValue({
|
||||||
|
config: {
|
||||||
|
multisig_control_contract: {
|
||||||
|
address: '0x1234',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(useEnvironment as unknown as jest.Mock).mockReturnValue({
|
||||||
|
ETHERSCAN_URL: 'https://etherscan.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<MultisigIncorrectNotice />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('multisig-contract-link')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'https://etherscan.io/address/0x1234'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('multisig-contract-link')).toHaveAttribute(
|
||||||
|
'title',
|
||||||
|
'0x1234'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('multisig-validators-learn-more')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render when config is not provided', () => {
|
||||||
|
(useEthereumConfig as jest.Mock).mockReturnValue({
|
||||||
|
config: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(<MultisigIncorrectNotice />);
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,49 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Callout, Intent, Link } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||||
|
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||||
|
import type { EthereumConfig } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
|
export const MultisigIncorrectNotice = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { config } = useEthereumConfig();
|
||||||
|
const { ETHERSCAN_URL } = useEnvironment();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = config[
|
||||||
|
'multisig_control_contract' as keyof EthereumConfig
|
||||||
|
] as {
|
||||||
|
address: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-10">
|
||||||
|
<Callout intent={Intent.Warning}>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
title={contract.address}
|
||||||
|
href={`${ETHERSCAN_URL}/address/${contract.address}`}
|
||||||
|
target="_blank"
|
||||||
|
data-testid="multisig-contract-link"
|
||||||
|
>
|
||||||
|
{t('multisigContractLink')}
|
||||||
|
</Link>{' '}
|
||||||
|
{t('multisigContractIncorrect')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<Link
|
||||||
|
href={DocsLinks?.VALIDATOR_SCORES_REWARDS}
|
||||||
|
target="_blank"
|
||||||
|
data-testid="multisig-validators-learn-more"
|
||||||
|
>
|
||||||
|
{t('learnMore')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -821,5 +821,8 @@
|
|||||||
"disclaimer2": "The Vega Governance App is free, public and open source software. Software upgrades may contain bugs or security vulnerabilities that might result in loss of functionality.",
|
"disclaimer2": "The Vega Governance App is free, public and open source software. Software upgrades may contain bugs or security vulnerabilities that might result in loss of functionality.",
|
||||||
"disclaimer3": "The Vega Governance App uses data obtained from nodes on the Vega Blockchain. The developers of the Vega Governance App do not operate or run the Vega Blockchain or any other blockchain.",
|
"disclaimer3": "The Vega Governance App uses data obtained from nodes on the Vega Blockchain. The developers of the Vega Governance App do not operate or run the Vega Blockchain or any other blockchain.",
|
||||||
"disclaimer4": "The Vega Governance App is provided “as is”. The developers of the Vega Governance App make no representations or warranties of any kind, whether express or implied, statutory or otherwise regarding the Vega Governance App. They disclaim all warranties of merchantability, quality, fitness for purpose. They disclaim all warranties that the Vega Governance App is free of harmful components or errors.",
|
"disclaimer4": "The Vega Governance App is provided “as is”. The developers of the Vega Governance App make no representations or warranties of any kind, whether express or implied, statutory or otherwise regarding the Vega Governance App. They disclaim all warranties of merchantability, quality, fitness for purpose. They disclaim all warranties that the Vega Governance App is free of harmful components or errors.",
|
||||||
"disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App."
|
"disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App.",
|
||||||
|
"multisigContractLink": "Ethereum Multisig Contract",
|
||||||
|
"multisigContractIncorrect": "is incorrectly configured. Validator and delegator rewards will be penalised until this is resolved.",
|
||||||
|
"learnMore": "Learn more"
|
||||||
}
|
}
|
||||||
|
70
apps/governance/src/lib/get-multisig-status-info.spec.ts
Normal file
70
apps/governance/src/lib/get-multisig-status-info.spec.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
getMultisigStatusInfo,
|
||||||
|
MultisigStatus,
|
||||||
|
} from './get-multisig-status-info';
|
||||||
|
import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
|
||||||
|
|
||||||
|
const createNode = (id: string, multisigScore: string) => ({
|
||||||
|
node: {
|
||||||
|
id,
|
||||||
|
stakedTotal: '1000',
|
||||||
|
rewardScore: { multisigScore },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMultisigStatus', () => {
|
||||||
|
it('should return MultisigStatus.noNodes when no nodes are present', () => {
|
||||||
|
const result = getMultisigStatusInfo({
|
||||||
|
epoch: { id: '1', validatorsConnection: { edges: [] } },
|
||||||
|
} as PreviousEpochQuery);
|
||||||
|
expect(result).toEqual({
|
||||||
|
multisigStatus: MultisigStatus.noNodes,
|
||||||
|
showMultisigStatusError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return MultisigStatus.correct when all nodes have multisigScore of 1', () => {
|
||||||
|
const result = getMultisigStatusInfo({
|
||||||
|
epoch: {
|
||||||
|
id: '1',
|
||||||
|
validatorsConnection: {
|
||||||
|
edges: [createNode('1', '1'), createNode('2', '1')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as PreviousEpochQuery);
|
||||||
|
expect(result).toEqual({
|
||||||
|
multisigStatus: MultisigStatus.correct,
|
||||||
|
showMultisigStatusError: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return MultisigStatus.nodeNeedsRemoving when all nodes have multisigScore of 0', () => {
|
||||||
|
const result = getMultisigStatusInfo({
|
||||||
|
epoch: {
|
||||||
|
id: '1',
|
||||||
|
validatorsConnection: {
|
||||||
|
edges: [createNode('1', '0'), createNode('2', '0')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as PreviousEpochQuery);
|
||||||
|
expect(result).toEqual({
|
||||||
|
multisigStatus: MultisigStatus.nodeNeedsRemoving,
|
||||||
|
showMultisigStatusError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return MultisigStatus.nodeNeedsAdding when some nodes have multisigScore of 0 and others have 1', () => {
|
||||||
|
const result = getMultisigStatusInfo({
|
||||||
|
epoch: {
|
||||||
|
id: '1',
|
||||||
|
validatorsConnection: {
|
||||||
|
edges: [createNode('1', '0'), createNode('2', '1')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as PreviousEpochQuery);
|
||||||
|
expect(result).toEqual({
|
||||||
|
multisigStatus: MultisigStatus.nodeNeedsAdding,
|
||||||
|
showMultisigStatusError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
42
apps/governance/src/lib/get-multisig-status-info.ts
Normal file
42
apps/governance/src/lib/get-multisig-status-info.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||||
|
import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
|
||||||
|
|
||||||
|
export enum MultisigStatus {
|
||||||
|
'correct' = 'correct',
|
||||||
|
'nodeNeedsAdding' = 'nodeNeedsAdding',
|
||||||
|
'nodeNeedsRemoving' = 'nodeNeedsRemoving ',
|
||||||
|
'noNodes' = 'noNodes',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMultisigStatusInfo = (
|
||||||
|
previousEpochData: PreviousEpochQuery
|
||||||
|
) => {
|
||||||
|
let status = MultisigStatus.noNodes;
|
||||||
|
|
||||||
|
const allNodesInPreviousEpoch = removePaginationWrapper(
|
||||||
|
previousEpochData?.epoch.validatorsConnection?.edges
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasZero = allNodesInPreviousEpoch.some(
|
||||||
|
(node) => Number(node?.rewardScore?.multisigScore) === 0
|
||||||
|
);
|
||||||
|
const hasOne = allNodesInPreviousEpoch.some(
|
||||||
|
(node) => Number(node?.rewardScore?.multisigScore) === 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasZero && hasOne) {
|
||||||
|
// If any individual node has 0 it means that node is missing from the multisig and needs to be added
|
||||||
|
status = MultisigStatus.nodeNeedsAdding;
|
||||||
|
} else if (hasZero) {
|
||||||
|
// If all nodes have 0 it means there is an incorrect address in the multisig that needs to be removed
|
||||||
|
status = MultisigStatus.nodeNeedsRemoving;
|
||||||
|
} else if (allNodesInPreviousEpoch.length > 0) {
|
||||||
|
// If all nodes have 1 it means the multisig is correct
|
||||||
|
status = MultisigStatus.correct;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showMultisigStatusError: status !== MultisigStatus.correct,
|
||||||
|
multisigStatus: status,
|
||||||
|
};
|
||||||
|
};
|
@ -23,6 +23,9 @@ import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
|||||||
import { DocsLinks } from '@vegaprotocol/environment';
|
import { DocsLinks } from '@vegaprotocol/environment';
|
||||||
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
|
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
|
||||||
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
|
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
|
||||||
|
import { usePreviousEpochQuery } from '../../staking/__generated__/PreviousEpoch';
|
||||||
|
import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info';
|
||||||
|
import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice';
|
||||||
|
|
||||||
type RewardsView = 'total' | 'individual';
|
type RewardsView = 'total' | 'individual';
|
||||||
|
|
||||||
@ -41,12 +44,25 @@ export const RewardsPage = () => {
|
|||||||
|
|
||||||
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
|
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
|
||||||
|
|
||||||
|
const { data: previousEpochData } = usePreviousEpochQuery({
|
||||||
|
variables: {
|
||||||
|
epochId: (Number(epochData?.epoch.id) - 1).toString(),
|
||||||
|
},
|
||||||
|
skip: !epochData?.epoch.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const multisigStatus = previousEpochData
|
||||||
|
? getMultisigStatusInfo(previousEpochData)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
params,
|
params,
|
||||||
loading: paramsLoading,
|
loading: paramsLoading,
|
||||||
error: paramsError,
|
error: paramsError,
|
||||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||||
|
|
||||||
|
console.log('params', params);
|
||||||
|
|
||||||
const payoutDuration = useMemo(() => {
|
const payoutDuration = useMemo(() => {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -78,14 +94,18 @@ export const RewardsPage = () => {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{payoutDuration ? (
|
{multisigStatus?.showMultisigStatusError ? (
|
||||||
|
<MultisigIncorrectNotice />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!multisigStatus?.showMultisigStatusError && payoutDuration ? (
|
||||||
<div className="my-8">
|
<div className="my-8">
|
||||||
<Callout
|
<Callout
|
||||||
title={t('rewardsCallout', {
|
title={t('rewardsCallout', {
|
||||||
duration: formatDistance(new Date(0), payoutDuration),
|
duration: formatDistance(new Date(0), payoutDuration),
|
||||||
})}
|
})}
|
||||||
headingLevel={3}
|
headingLevel={3}
|
||||||
intent={Intent.Warning}
|
intent={Intent.Primary}
|
||||||
>
|
>
|
||||||
<p className="mb-0">{t('rewardsCalloutDetail')}</p>
|
<p className="mb-0">{t('rewardsCalloutDetail')}</p>
|
||||||
</Callout>
|
</Callout>
|
||||||
|
@ -7,6 +7,8 @@ import { ValidatorTables } from './validator-tables';
|
|||||||
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { ENV } from '../../../config';
|
import { ENV } from '../../../config';
|
||||||
|
import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info';
|
||||||
|
import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice';
|
||||||
|
|
||||||
export const EpochData = () => {
|
export const EpochData = () => {
|
||||||
// errorPolicy due to vegaprotocol/vega issue 5898
|
// errorPolicy due to vegaprotocol/vega issue 5898
|
||||||
@ -46,12 +48,20 @@ export const EpochData = () => {
|
|||||||
userStakingRefetch();
|
userStakingRefetch();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const multisigStatus = previousEpochData
|
||||||
|
? getMultisigStatusInfo(previousEpochData)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
loading={nodesLoading || userStakingLoading}
|
loading={nodesLoading || userStakingLoading}
|
||||||
error={nodesError || userStakingError}
|
error={nodesError || userStakingError}
|
||||||
data={nodesData}
|
data={nodesData}
|
||||||
>
|
>
|
||||||
|
{multisigStatus?.showMultisigStatusError ? (
|
||||||
|
<MultisigIncorrectNotice />
|
||||||
|
) : null}
|
||||||
|
|
||||||
{nodesData?.epoch &&
|
{nodesData?.epoch &&
|
||||||
nodesData.epoch.timestamps.start &&
|
nodesData.epoch.timestamps.start &&
|
||||||
nodesData?.epoch.timestamps.expiry && (
|
nodesData?.epoch.timestamps.expiry && (
|
||||||
|
@ -72,6 +72,7 @@ export const DocsLinks = VEGA_DOCS_URL
|
|||||||
POSITION_RESOLUTION: `${VEGA_DOCS_URL}/concepts/trading-on-vega/market-protections#position-resolution`,
|
POSITION_RESOLUTION: `${VEGA_DOCS_URL}/concepts/trading-on-vega/market-protections#position-resolution`,
|
||||||
LIQUIDITY: `${VEGA_DOCS_URL}/concepts/liquidity/provision`,
|
LIQUIDITY: `${VEGA_DOCS_URL}/concepts/liquidity/provision`,
|
||||||
WITHDRAWAL_LIMITS: `${VEGA_DOCS_URL}/concepts/assets/deposits-withdrawals#withdrawal-limits`,
|
WITHDRAWAL_LIMITS: `${VEGA_DOCS_URL}/concepts/assets/deposits-withdrawals#withdrawal-limits`,
|
||||||
|
VALIDATOR_SCORES_REWARDS: `${VEGA_DOCS_URL}/concepts/vega-chain/validator-scores-and-rewards`,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user