Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0e157fae | ||
|
|
7ea7362a7d | ||
|
|
bbfe42ddb1 | ||
|
|
b163af3e8a | ||
|
|
82df401611 | ||
|
|
4733bc169c | ||
|
|
2b91aebc2a | ||
|
|
375f4da541 | ||
|
|
bc413ed314 | ||
|
|
2fab3daebd | ||
|
|
2e07ada966 | ||
|
|
b81c4bc948 | ||
|
|
4f8d6bd876 | ||
|
|
52e5a37da3 | ||
|
|
22599673ec | ||
|
|
3c6a806ad3 | ||
|
|
7101d49d1d | ||
|
|
72e0cb76aa | ||
|
|
ca64516a52 | ||
|
|
55d692ea6f | ||
|
|
f235c03abe | ||
|
|
546deb0e1c | ||
|
|
042919eca9 | ||
|
|
c4a56e0de3 | ||
|
|
3c3bfb7dac | ||
|
|
196ba78806 | ||
|
|
53ac2dadee | ||
|
|
e532f88daa | ||
|
|
7b06c05853 | ||
|
|
a92fe92778 | ||
|
|
b8725a7fa8 | ||
|
|
f556247e1a | ||
|
|
48d6be0adf | ||
|
|
be6f395ce4 | ||
|
|
9a37572f51 | ||
|
|
19fb406d49 | ||
|
|
a2a04c57d2 | ||
|
|
1f08e8225a | ||
|
|
5fff6ba3f7 | ||
|
|
6cacc46a74 | ||
|
|
98ff4f3c04 | ||
|
|
8268acac6d | ||
|
|
4ad1a47ded | ||
|
|
fea97aac19 | ||
|
|
f652292e02 | ||
|
|
9195bf8c91 | ||
|
|
cd481512f3 | ||
|
|
8fe6f3f5f2 | ||
|
|
1c2389dee5 | ||
|
|
1e6a2debfc | ||
|
|
fc2773d748 | ||
|
|
b2860121e5 | ||
|
|
db5e5ee782 | ||
|
|
0d3bcf05a1 | ||
|
|
c7dd5e846a | ||
|
|
61e5941751 | ||
|
|
8db286fa93 | ||
|
|
e5635cae61 | ||
|
|
b01c67ced5 | ||
|
|
636b1f98db | ||
|
|
c31a927526 | ||
|
|
5803d6e890 | ||
|
|
cb0dd17839 | ||
|
|
496b0b5a90 |
@@ -10,7 +10,7 @@ on:
|
||||
inputs:
|
||||
console-test-branch:
|
||||
type: choice
|
||||
description: 'main: v0.73.5, develop: v0.73.5'
|
||||
description: 'main: v0.73.13, develop: v0.74.0'
|
||||
options:
|
||||
- main
|
||||
- develop
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rm package.json
|
||||
npm install --no-save @commitlint/cli @commitlint/config-conventional @commitlint/config-nx-scopes nx
|
||||
npm install --no-save @commitlint/cli@16.3.0 @commitlint/config-conventional@18.6.1 @commitlint/config-nx-scopes@18.6.1 nx@17.1.2
|
||||
|
||||
- name: Check PR title
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx @commitlint/cli@16.3.0 --config ./commitlint.config-ci.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getNewAssetTxBody } from '../support/governance.functions';
|
||||
|
||||
context('Proposal page', { tags: '@smoke' }, function () {
|
||||
describe('Verify elements on page', function () {
|
||||
describe.skip('Verify elements on page', function () {
|
||||
const proposalHeading = 'proposals-heading';
|
||||
const dateTimeRegex =
|
||||
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
|
||||
|
||||
@@ -7,6 +7,7 @@ export type AssetBalanceProps = {
|
||||
price: string;
|
||||
showAssetLink?: boolean;
|
||||
showAssetSymbol?: boolean;
|
||||
rounded?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -18,12 +19,17 @@ const AssetBalance = ({
|
||||
price,
|
||||
showAssetLink = true,
|
||||
showAssetSymbol = false,
|
||||
rounded = false,
|
||||
}: AssetBalanceProps) => {
|
||||
const { data: asset, loading } = useAssetDataProvider(assetId);
|
||||
|
||||
const label =
|
||||
!loading && asset && asset.decimals
|
||||
? addDecimalsFixedFormatNumber(price, asset.decimals)
|
||||
? addDecimalsFixedFormatNumber(
|
||||
price,
|
||||
asset.decimals,
|
||||
rounded ? 0 : undefined
|
||||
)
|
||||
: price;
|
||||
|
||||
return (
|
||||
|
||||
@@ -41,6 +41,7 @@ export const Header = () => {
|
||||
Routes.ASSETS,
|
||||
Routes.MARKETS,
|
||||
Routes.GOVERNANCE,
|
||||
Routes.TREASURY,
|
||||
Routes.NETWORK_PARAMETERS,
|
||||
Routes.GENESIS,
|
||||
].map((n) => pages.find((r) => r.path === n))
|
||||
|
||||
@@ -32,9 +32,17 @@ export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
|
||||
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
truncate?: boolean;
|
||||
networkLabel?: string;
|
||||
truncateLength?: number;
|
||||
};
|
||||
|
||||
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||
const PartyLink = ({
|
||||
id,
|
||||
truncate = false,
|
||||
truncateLength = 4,
|
||||
networkLabel = t('Network'),
|
||||
...props
|
||||
}: PartyLinkProps) => {
|
||||
const { data } = useExplorerNodeNamesQuery();
|
||||
const name = useMemo(() => getNameForParty(id, data), [data, id]);
|
||||
const useName = name !== id;
|
||||
@@ -44,7 +52,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
|
||||
return (
|
||||
<span className="font-mono" data-testid="network">
|
||||
{t('Network')}
|
||||
{networkLabel}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -70,7 +78,11 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||
{useName ? (
|
||||
name
|
||||
) : (
|
||||
<Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
|
||||
<Hash
|
||||
text={
|
||||
truncate ? truncateMiddle(id, truncateLength, truncateLength) : id
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
@@ -175,6 +175,7 @@ describe('Amend order details', () => {
|
||||
|
||||
const res = renderExistingAmend('123', 1, amend);
|
||||
expect(await res.findByText('New size')).toBeInTheDocument();
|
||||
expect(await res.findByText('Size ±')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders Reference if provided', async () => {
|
||||
|
||||
@@ -82,7 +82,7 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
|
||||
{amend.sizeDelta && amend.sizeDelta !== '0' ? (
|
||||
<div className="mb-12 md:mb-0">
|
||||
<h2 className="text-dark mb-4 text-2xl font-bold">
|
||||
{t('New size')}
|
||||
{t('Size ±')}
|
||||
</h2>
|
||||
<h5
|
||||
className={`mb-0 text-lg font-medium capitalize text-gray-500 ${getSideDeltaColour(
|
||||
@@ -93,6 +93,16 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
|
||||
</h5>
|
||||
</div>
|
||||
) : null}
|
||||
{o && (
|
||||
<div className="">
|
||||
<h2 className="text-dark mb-4 text-2xl font-bold">
|
||||
{t('New size')}
|
||||
</h2>
|
||||
<h5 className="mb-0 text-lg font-medium text-gray-500">
|
||||
{o ? o.size : null}
|
||||
</h5>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{amend.price && amend.price !== '0' ? (
|
||||
<div className="">
|
||||
|
||||
@@ -64,7 +64,9 @@ export const ProposalSummary = ({
|
||||
return (
|
||||
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5">
|
||||
{id && <ProposalStatusIcon id={id} />}
|
||||
{rationale?.title && <h1 className="text-xl pb-1">{rationale.title}</h1>}
|
||||
{rationale?.title && (
|
||||
<h1 className="text-xl pb-1 break-all">{rationale.title}</h1>
|
||||
)}
|
||||
{rationale?.description && (
|
||||
<div className="pt-2 text-sm leading-tight">
|
||||
<ReactMarkdown
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
query ExplorerTransferStatus($id: ID!) {
|
||||
transfer(id: $id) {
|
||||
transfer {
|
||||
reference
|
||||
timestamp
|
||||
status
|
||||
reason
|
||||
fromAccountType
|
||||
from
|
||||
to
|
||||
toAccountType
|
||||
asset {
|
||||
id
|
||||
}
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerTransferStatusQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerTransferStatusQuery = { __typename?: 'Query', transfer?: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', reference?: string | null, timestamp: any, status: Types.TransferStatus, reason?: string | null, fromAccountType: Types.AccountType, from: string, to: string, toAccountType: Types.AccountType, amount: string, asset?: { __typename?: 'Asset', id: string } | null } } | null };
|
||||
|
||||
|
||||
export const ExplorerTransferStatusDocument = gql`
|
||||
query ExplorerTransferStatus($id: ID!) {
|
||||
transfer(id: $id) {
|
||||
transfer {
|
||||
reference
|
||||
timestamp
|
||||
status
|
||||
reason
|
||||
fromAccountType
|
||||
from
|
||||
to
|
||||
toAccountType
|
||||
asset {
|
||||
id
|
||||
}
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerTransferStatusQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerTransferStatusQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerTransferStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerTransferStatusQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerTransferStatusQuery(baseOptions: Apollo.QueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
|
||||
}
|
||||
export function useExplorerTransferStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
|
||||
}
|
||||
export type ExplorerTransferStatusQueryHookResult = ReturnType<typeof useExplorerTransferStatusQuery>;
|
||||
export type ExplorerTransferStatusLazyQueryHookResult = ReturnType<typeof useExplorerTransferStatusLazyQuery>;
|
||||
export type ExplorerTransferStatusQueryResult = Apollo.QueryResult<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>;
|
||||
+2
-2
@@ -111,7 +111,7 @@ export function TransferParticipants({
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 9"
|
||||
className="fill-vega-light-100 dark:fill-black"
|
||||
className="fill-white dark:fill-black"
|
||||
>
|
||||
<path d="M0,0L8,9l8,-9Z" />
|
||||
</svg>
|
||||
@@ -120,7 +120,7 @@ export function TransferParticipants({
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 9"
|
||||
className="fill-vega-light-100 dark:fill-vega-dark-200"
|
||||
className="fill-vega-light-200 dark:fill-vega-dark-200"
|
||||
>
|
||||
<path d="M0,0L8,9l8,-9Z" />
|
||||
</svg>
|
||||
|
||||
+178
-52
@@ -1,97 +1,223 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { AssetLink, MarketLink } from '../../../../links';
|
||||
import { headerClasses, wrapperClasses } from '../transfer-details';
|
||||
import type { components } from '../../../../../../types/explorer';
|
||||
import type { Recurring } from '../transfer-details';
|
||||
import { DispatchMetricLabels } from '@vegaprotocol/types';
|
||||
|
||||
import {
|
||||
DispatchMetricLabels,
|
||||
DistributionStrategy,
|
||||
} from '@vegaprotocol/types';
|
||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
export type Metric = components['schemas']['vegaDispatchMetric'];
|
||||
export type Strategy = components['schemas']['vegaDispatchStrategy'];
|
||||
|
||||
export const wrapperClasses = 'border pv-2 w-full flex-auto basis-full';
|
||||
export const headerClasses =
|
||||
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 text-center text-xl py-2 font-alpha calt';
|
||||
|
||||
const metricLabels: Record<Metric, string> = {
|
||||
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
|
||||
...DispatchMetricLabels,
|
||||
};
|
||||
|
||||
// Maps the two (non-null) values of entityScope to the icon that represents it
|
||||
const entityScopeIcons: Record<
|
||||
string,
|
||||
typeof VegaIconNames[keyof typeof VegaIconNames]
|
||||
> = {
|
||||
ENTITY_SCOPE_INDIVIDUALS: VegaIconNames.MAN,
|
||||
ENTITY_SCOPE_TEAMS: VegaIconNames.TEAM,
|
||||
};
|
||||
|
||||
const distributionStrategyLabel: Record<DistributionStrategy, string> = {
|
||||
[DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA]: 'Pro Rata',
|
||||
[DistributionStrategy.DISTRIBUTION_STRATEGY_RANK]: 'Ranked',
|
||||
};
|
||||
|
||||
interface TransferRewardsProps {
|
||||
recurring: Recurring;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for a transfer. These can vary quite
|
||||
* widely, essentially every field can be null.
|
||||
* Renders recurring transfers/game details in a way that is, perhaps, easy to understand
|
||||
*
|
||||
* @param transfer A recurring transfer object
|
||||
*/
|
||||
export function TransferRewards({ recurring }: TransferRewardsProps) {
|
||||
const metric =
|
||||
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
|
||||
|
||||
if (!recurring || !recurring.dispatchStrategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Destructure to make things a bit more readable
|
||||
const {
|
||||
entityScope,
|
||||
individualScope,
|
||||
teamScope,
|
||||
distributionStrategy,
|
||||
lockPeriod,
|
||||
markets,
|
||||
stakingRequirement,
|
||||
windowLength,
|
||||
notionalTimeWeightedAveragePositionRequirement,
|
||||
rankTable,
|
||||
nTopPerformers,
|
||||
} = recurring.dispatchStrategy;
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
|
||||
<ul className="relative block rounded-lg py-6 text-center p-6">
|
||||
{recurring.dispatchStrategy.assetForMetric ? (
|
||||
<h2 className={headerClasses}>{getRewardTitle(entityScope)}</h2>
|
||||
<ul className="relative block rounded-lg py-6 text-left p-6">
|
||||
{entityScope && entityScopeIcons[entityScope] ? (
|
||||
<li>
|
||||
<strong>{t('Asset')}</strong>:{' '}
|
||||
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
|
||||
<strong>{t('Scope')}</strong>:{' '}
|
||||
<VegaIcon name={entityScopeIcons[entityScope]} />
|
||||
|
||||
{individualScope ? individualScopeLabels[individualScope] : null}
|
||||
{getScopeLabel(entityScope, teamScope)}
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
|
||||
</li>
|
||||
{recurring.dispatchStrategy.markets &&
|
||||
recurring.dispatchStrategy.markets.length > 0 ? (
|
||||
{recurring.dispatchStrategy &&
|
||||
recurring.dispatchStrategy.assetForMetric && (
|
||||
<li>
|
||||
<strong>{t('Asset for metric')}</strong>:{' '}
|
||||
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
|
||||
</li>
|
||||
)}
|
||||
{recurring.dispatchStrategy.metric &&
|
||||
metricLabels[recurring.dispatchStrategy.metric] && (
|
||||
<li>
|
||||
<strong>{t('Metric')}</strong>:{' '}
|
||||
{metricLabels[recurring.dispatchStrategy.metric]}
|
||||
</li>
|
||||
)}
|
||||
{lockPeriod && (
|
||||
<li>
|
||||
<strong>{t('Reward lock')}</strong>:
|
||||
{recurring.dispatchStrategy.lockPeriod}{' '}
|
||||
{recurring.dispatchStrategy.lockPeriod === '1'
|
||||
? t('epoch')
|
||||
: t('epochs')}
|
||||
</li>
|
||||
)}
|
||||
|
||||
{markets && markets.length > 0 ? (
|
||||
<li>
|
||||
<strong>{t('Markets in scope')}</strong>:
|
||||
<ul>
|
||||
{recurring.dispatchStrategy.markets.map((m) => (
|
||||
<li key={m}>
|
||||
<ul className="inline-block ml-1">
|
||||
{markets.map((m) => (
|
||||
<li key={m} className="inline-block mr-2">
|
||||
<MarketLink id={m} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<strong>{t('Factor')}</strong>: {recurring.factor}
|
||||
</li>
|
||||
|
||||
{stakingRequirement && stakingRequirement !== '0' ? (
|
||||
<li>
|
||||
<strong>{t('Staking requirement')}</strong>: {stakingRequirement}
|
||||
</li>
|
||||
) : null}
|
||||
|
||||
{windowLength && windowLength !== '0' ? (
|
||||
<li>
|
||||
<strong>{t('Window length')}</strong>:{' '}
|
||||
{recurring.dispatchStrategy.windowLength}{' '}
|
||||
{recurring.dispatchStrategy.windowLength === '1'
|
||||
? t('epoch')
|
||||
: t('epochs')}
|
||||
</li>
|
||||
) : null}
|
||||
|
||||
{notionalTimeWeightedAveragePositionRequirement &&
|
||||
notionalTimeWeightedAveragePositionRequirement !== '' ? (
|
||||
<li>
|
||||
<strong>{t('Notional TWAP')}</strong>:{' '}
|
||||
{notionalTimeWeightedAveragePositionRequirement}
|
||||
</li>
|
||||
) : null}
|
||||
|
||||
{nTopPerformers && (
|
||||
<li>
|
||||
<strong>{t('Elligible team members:')}</strong> top{' '}
|
||||
{`${formatNumber(Number(nTopPerformers) * 100, 0)}%`}
|
||||
</li>
|
||||
)}
|
||||
|
||||
{distributionStrategy &&
|
||||
distributionStrategy !== 'DISTRIBUTION_STRATEGY_UNSPECIFIED' && (
|
||||
<li>
|
||||
<strong>{t('Distribution strategy')}</strong>:{' '}
|
||||
{distributionStrategyLabel[distributionStrategy]}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<div className="px-6 pt-1 pb-5">
|
||||
{rankTable && rankTable.length > 0 ? (
|
||||
<table className="border-collapse border border-gray-400 ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-gray-300 bg-gray-300 px-3">
|
||||
<strong>{t('Start rank')}</strong>
|
||||
</th>
|
||||
<th className="border border-gray-300 bg-gray-300 px-3">
|
||||
<strong>{t('Share of reward pool')}</strong>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rankTable.map((row, i) => {
|
||||
return (
|
||||
<tr key={`rank-${i}`}>
|
||||
<td className="border border-slate-300 text-center">
|
||||
{row.startRank}
|
||||
</td>
|
||||
<td className="border border-slate-300 text-center">
|
||||
{row.shareRatio}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TransferRecurringStrategyProps {
|
||||
strategy: Strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple renderer for a dispatch strategy in a recurring transfer
|
||||
*
|
||||
* @param strategy Dispatch strategy object
|
||||
*/
|
||||
export function TransferRecurringStrategy({
|
||||
strategy,
|
||||
}: TransferRecurringStrategyProps) {
|
||||
if (!strategy) {
|
||||
return null;
|
||||
export function getScopeLabel(
|
||||
scope: components['schemas']['vegaEntityScope'] | undefined,
|
||||
teamScope: readonly string[] | undefined
|
||||
): string {
|
||||
if (scope === 'ENTITY_SCOPE_TEAMS') {
|
||||
if (teamScope && teamScope.length !== 0) {
|
||||
return ` ${teamScope.length} teams`;
|
||||
} else {
|
||||
return t('All teams');
|
||||
}
|
||||
} else if (scope === 'ENTITY_SCOPE_INDIVIDUALS') {
|
||||
return t('Individuals');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{strategy.assetForMetric ? (
|
||||
<li>
|
||||
<strong>{t('Asset for metric')}</strong>:{' '}
|
||||
<AssetLink assetId={strategy.assetForMetric} />
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<strong>{t('Metric')}</strong>: {strategy.metric}
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export function getRewardTitle(
|
||||
scope?: components['schemas']['vegaEntityScope']
|
||||
) {
|
||||
if (scope === 'ENTITY_SCOPE_TEAMS') {
|
||||
return t('Game');
|
||||
}
|
||||
return t('Reward metrics');
|
||||
}
|
||||
|
||||
const individualScopeLabels: Record<
|
||||
components['schemas']['vegaIndividualScope'],
|
||||
string
|
||||
> = {
|
||||
// Unspecified and All are not rendered
|
||||
INDIVIDUAL_SCOPE_UNSPECIFIED: '',
|
||||
INDIVIDUAL_SCOPE_ALL: '',
|
||||
INDIVIDUAL_SCOPE_IN_TEAM: '(in team)',
|
||||
INDIVIDUAL_SCOPE_NOT_IN_TEAM: '(not in team)',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { headerClasses, wrapperClasses } from '../transfer-details';
|
||||
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import type { IconName } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ApolloError } from '@apollo/client';
|
||||
import { TransferStatus, TransferStatusMapping } from '@vegaprotocol/types';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
|
||||
interface TransferStatusProps {
|
||||
status: TransferStatus | undefined;
|
||||
error: ApolloError | undefined;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for a transfer. These can vary quite
|
||||
* widely, essentially every field can be null.
|
||||
*
|
||||
* @param transfer A recurring transfer object
|
||||
*/
|
||||
export function TransferStatusView({ status, loading }: TransferStatusProps) {
|
||||
if (!status) {
|
||||
status = TransferStatus.STATUS_PENDING;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<h2 className={headerClasses}>{t('Status')}</h2>
|
||||
<div className="relative block rounded-lg py-6 text-center p-6">
|
||||
{loading ? (
|
||||
<div className="leading-10 mt-12">
|
||||
<Loader size={'small'} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="leading-10 my-2">
|
||||
<TransferStatusIcon status={status} />
|
||||
</p>
|
||||
<p className="leading-10 my-2">{TransferStatusMapping[status]}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TransferStatusIconProps {
|
||||
status: TransferStatus;
|
||||
}
|
||||
|
||||
export function TransferStatusIcon({ status }: TransferStatusIconProps) {
|
||||
return (
|
||||
<span title={TransferStatusMapping[status]}>
|
||||
<Icon
|
||||
name={getIconForStatus(status)}
|
||||
className={getColourForStatus(status)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple mapping from status to icon name
|
||||
* @param status TransferStatus
|
||||
* @returns IconName
|
||||
*/
|
||||
export function getIconForStatus(status: TransferStatus): IconName {
|
||||
switch (status) {
|
||||
case TransferStatus.STATUS_PENDING:
|
||||
return IconNames.TIME;
|
||||
case TransferStatus.STATUS_DONE:
|
||||
return IconNames.TICK;
|
||||
case TransferStatus.STATUS_REJECTED:
|
||||
return IconNames.CROSS;
|
||||
case TransferStatus.STATUS_CANCELLED:
|
||||
return IconNames.CROSS;
|
||||
default:
|
||||
return IconNames.TIME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple mapping from status to colour
|
||||
* @param status TransferStatus
|
||||
* @returns string Tailwind classname
|
||||
*/
|
||||
export function getColourForStatus(status: TransferStatus): string {
|
||||
switch (status) {
|
||||
case TransferStatus.STATUS_PENDING:
|
||||
return 'text-yellow-500';
|
||||
case TransferStatus.STATUS_DONE:
|
||||
return 'text-green-500';
|
||||
case TransferStatus.STATUS_REJECTED:
|
||||
return 'text-red-500';
|
||||
case TransferStatus.STATUS_CANCELLED:
|
||||
return 'text-red-600';
|
||||
default:
|
||||
return 'text-yellow-500';
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@ import type { components } from '../../../../../types/explorer';
|
||||
import { TransferRepeat } from './blocks/transfer-repeat';
|
||||
import { TransferRewards } from './blocks/transfer-rewards';
|
||||
import { TransferParticipants } from './blocks/transfer-participants';
|
||||
import { useExplorerTransferStatusQuery } from './__generated__/Transfer';
|
||||
import { TransferStatusView } from './blocks/transfer-status';
|
||||
import { TransferStatus } from '@vegaprotocol/types';
|
||||
|
||||
export type Recurring = components['schemas']['commandsv1RecurringTransfer'];
|
||||
export type Metric = components['schemas']['vegaDispatchMetric'];
|
||||
|
||||
export const wrapperClasses =
|
||||
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
|
||||
'border border-vega-light-150 dark:border-vega-dark-200 pv-2 w-full sm:w-1/3 basis-1/3';
|
||||
export const headerClasses =
|
||||
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha calt';
|
||||
|
||||
@@ -16,6 +19,7 @@ export type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
interface TransferDetailsProps {
|
||||
transfer: Transfer;
|
||||
from: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,13 +28,24 @@ interface TransferDetailsProps {
|
||||
*
|
||||
* @param transfer A recurring transfer object
|
||||
*/
|
||||
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
|
||||
export function TransferDetails({ transfer, from, id }: TransferDetailsProps) {
|
||||
const recurring = transfer.recurring;
|
||||
|
||||
// Currently all this is passed in to TransferStatus, but the extra details
|
||||
// may be useful in the future.
|
||||
const { data, error, loading } = useExplorerTransferStatusQuery({
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
const status = error
|
||||
? TransferStatus.STATUS_REJECTED
|
||||
: data?.transfer?.transfer.status;
|
||||
|
||||
return (
|
||||
<div className="flex gap-5 flex-wrap">
|
||||
<div className="flex flex-wrap">
|
||||
<TransferParticipants from={from} transfer={transfer} />
|
||||
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
|
||||
<TransferStatusView status={status} error={error} loading={loading} />
|
||||
{recurring && recurring.dispatchStrategy ? (
|
||||
<TransferRewards recurring={transfer.recurring} />
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
getScopeLabel,
|
||||
getRewardTitle,
|
||||
TransferRewards,
|
||||
} from './blocks/transfer-rewards';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import type { Recurring } from './transfer-details';
|
||||
import {
|
||||
DispatchMetric,
|
||||
DistributionStrategy,
|
||||
EntityScope,
|
||||
IndividualScope,
|
||||
} from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
describe('getScopeLabel', () => {
|
||||
it('should return the correct label for ENTITY_SCOPE_TEAMS with teamScope', () => {
|
||||
const scope = 'ENTITY_SCOPE_TEAMS';
|
||||
const teamScope = ['team1', 'team2', 'team3'];
|
||||
const expectedLabel = ' 3 teams';
|
||||
|
||||
const result = getScopeLabel(scope, teamScope);
|
||||
|
||||
expect(result).toEqual(expectedLabel);
|
||||
});
|
||||
|
||||
it('should return the correct label for ENTITY_SCOPE_TEAMS without teamScope', () => {
|
||||
const scope = 'ENTITY_SCOPE_TEAMS';
|
||||
const teamScope = undefined;
|
||||
const expectedLabel = 'All teams';
|
||||
|
||||
const result = getScopeLabel(scope, teamScope);
|
||||
|
||||
expect(result).toEqual(expectedLabel);
|
||||
});
|
||||
|
||||
it('should return the correct label for ENTITY_SCOPE_INDIVIDUALS', () => {
|
||||
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
|
||||
const teamScope = undefined;
|
||||
const expectedLabel = 'Individuals';
|
||||
|
||||
const result = getScopeLabel(scope, teamScope);
|
||||
|
||||
expect(result).toEqual(expectedLabel);
|
||||
});
|
||||
|
||||
it('should return an empty string for unknown scope', () => {
|
||||
const scope = 'UNKNOWN_SCOPE';
|
||||
const teamScope = undefined;
|
||||
const expectedLabel = '';
|
||||
|
||||
const result = getScopeLabel(
|
||||
scope as unknown as components['schemas']['vegaEntityScope'],
|
||||
teamScope
|
||||
);
|
||||
|
||||
expect(result).toEqual(expectedLabel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRewardTitle', () => {
|
||||
it('should return the correct title for ENTITY_SCOPE_TEAMS', () => {
|
||||
const scope = 'ENTITY_SCOPE_TEAMS';
|
||||
const expectedTitle = 'Game';
|
||||
|
||||
const result = getRewardTitle(scope);
|
||||
|
||||
expect(result).toEqual(expectedTitle);
|
||||
});
|
||||
|
||||
it('should return the correct title for other scopes', () => {
|
||||
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
|
||||
const expectedTitle = 'Reward metrics';
|
||||
|
||||
const result = getRewardTitle(scope);
|
||||
|
||||
expect(result).toEqual(expectedTitle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TransferRewards', () => {
|
||||
it('should render nothing if recurring dispatchStrategy is not provided', () => {
|
||||
const { container } = render(
|
||||
<TransferRewards recurring={null as unknown as Recurring} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should render nothing if recurring.dispatchStrategy is not provided', () => {
|
||||
const { container } = render(
|
||||
<TransferRewards recurring={{} as unknown as Recurring} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should render the reward details correctly', () => {
|
||||
const recurring = {
|
||||
dispatchStrategy: {
|
||||
metric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
|
||||
assetForMetric: '123',
|
||||
entityScope: EntityScope.ENTITY_SCOPE_TEAMS,
|
||||
individualScope: IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM,
|
||||
teamScope: [],
|
||||
distributionStrategy:
|
||||
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
|
||||
lockPeriod: 'lockPeriod',
|
||||
markets: ['market1', 'market2'],
|
||||
stakingRequirement: '1',
|
||||
windowLength: 'windowLength',
|
||||
notionalTimeWeightedAveragePositionRequirement:
|
||||
'notionalTimeWeightedAveragePositionRequirement',
|
||||
rankTable: [
|
||||
{ startRank: 1, shareRatio: 0.2 },
|
||||
{ startRank: 2, shareRatio: 0.3 },
|
||||
],
|
||||
nTopPerformers: 'nTopPerformers',
|
||||
},
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<TransferRewards recurring={recurring} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(getByText('Game')).toBeInTheDocument();
|
||||
expect(getByText('Scope')).toBeInTheDocument();
|
||||
expect(getByText('Asset for metric')).toBeInTheDocument();
|
||||
expect(getByText('Metric')).toBeInTheDocument();
|
||||
expect(getByText('Reward lock')).toBeInTheDocument();
|
||||
expect(getByText('Markets in scope')).toBeInTheDocument();
|
||||
expect(getByText('Staking requirement')).toBeInTheDocument();
|
||||
expect(getByText('Window length')).toBeInTheDocument();
|
||||
expect(getByText('Notional TWAP')).toBeInTheDocument();
|
||||
expect(getByText('Elligible team members:')).toBeInTheDocument();
|
||||
expect(getByText('Distribution strategy')).toBeInTheDocument();
|
||||
expect(getByText('Start rank')).toBeInTheDocument();
|
||||
expect(getByText('Share of reward pool')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render a rank table if recurring.dispatchStrategy.rankTable is not provided', () => {
|
||||
const recurring = {
|
||||
dispatchStrategy: {
|
||||
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
|
||||
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
|
||||
teamScope: ['team1', 'team2', 'team3'],
|
||||
distributionStrategy:
|
||||
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
|
||||
lockPeriod: 'lockPeriod',
|
||||
markets: ['market1', 'market2'],
|
||||
stakingRequirement: 'stakingRequirement',
|
||||
windowLength: 'windowLength',
|
||||
notionalTimeWeightedAveragePositionRequirement:
|
||||
'notionalTimeWeightedAveragePositionRequirement',
|
||||
nTopPerformers: 'nTopPerformers',
|
||||
},
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<TransferRewards recurring={recurring} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(container.querySelector('table')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,8 @@ import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new
|
||||
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
|
||||
import { MarketLink } from '../../links';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { TransferDetails } from './transfer/transfer-details';
|
||||
import { proposalToTransfer } from '../lib/proposal-to-transfer';
|
||||
|
||||
export type Proposal = components['schemas']['v1ProposalSubmission'];
|
||||
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
|
||||
@@ -104,6 +106,12 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
|
||||
? ProposalSignatureBundleNewAsset
|
||||
: ProposalSignatureBundleUpdateAsset;
|
||||
|
||||
let transfer, from;
|
||||
if (proposal.terms?.newTransfer?.changes) {
|
||||
transfer = proposalToTransfer(proposal.terms?.newTransfer.changes);
|
||||
from = proposal.terms.newTransfer.changes.source;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
@@ -149,14 +157,26 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
|
||||
</>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
|
||||
<ProposalSummary
|
||||
id={deterministicId}
|
||||
rationale={proposal.rationale}
|
||||
terms={proposal?.terms}
|
||||
/>
|
||||
|
||||
{proposalRequiresSignatureBundle(proposal) && (
|
||||
<SignatureBundleComponent id={deterministicId} tx={tx} />
|
||||
)}
|
||||
|
||||
{transfer && (
|
||||
<div className="mt-8">
|
||||
<TransferDetails
|
||||
transfer={transfer}
|
||||
from={from || ''}
|
||||
id={deterministicId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
SPECIAL_CASE_NETWORK_ID,
|
||||
} from '../../links/party-link/party-link';
|
||||
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||
import Hash from '../../links/hash';
|
||||
|
||||
type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
@@ -60,7 +61,7 @@ export const TxDetailsTransfer = ({
|
||||
}
|
||||
|
||||
const from = txData.submitter;
|
||||
|
||||
const id = txSignatureToDeterministicId(txData.signature.value);
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
@@ -71,7 +72,7 @@ export const TxDetailsTransfer = ({
|
||||
<TableRow modifier="bordered" data-testid="id">
|
||||
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
|
||||
<TableCell>
|
||||
{txSignatureToDeterministicId(txData.signature.value)}
|
||||
<Hash text={id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TxDetailsShared
|
||||
@@ -105,7 +106,7 @@ export const TxDetailsTransfer = ({
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
<TransferDetails from={from} transfer={transfer} />
|
||||
<TransferDetails from={from} transfer={transfer} id={id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { components } from '../../../../types/explorer';
|
||||
|
||||
type TransferProposal = components['schemas']['vegaNewTransferConfiguration'];
|
||||
type ActualTransfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
/**
|
||||
* Converts a governance proposal for a transfer in to a transfer command that the
|
||||
* TransferDetails component can then render. The types are very similar, but do not
|
||||
* map precisely to each other due to some missing fields and some different field
|
||||
* names.
|
||||
*
|
||||
* @param proposal Governance proposal for a transfer
|
||||
* @returns transfer a Transfer object as if it had been submitted
|
||||
*/
|
||||
export function proposalToTransfer(proposal: TransferProposal): ActualTransfer {
|
||||
return {
|
||||
amount: proposal.amount,
|
||||
asset: proposal.asset,
|
||||
// On a transfer, 'from' is determined by the submitter, so there is no 'from' field
|
||||
// fromAccountType does exist and is just named differently on the proposal
|
||||
fromAccountType: proposal.sourceType,
|
||||
oneOff: proposal.oneOff,
|
||||
recurring: proposal.recurring,
|
||||
// There is no reference applied on governance initiated transfers
|
||||
reference: '',
|
||||
to: proposal.destination,
|
||||
toAccountType: proposal.destinationType,
|
||||
};
|
||||
}
|
||||
@@ -12,4 +12,5 @@ export const Routes = {
|
||||
ORACLES: 'oracles',
|
||||
NETWORK_PARAMETERS: 'network-parameters',
|
||||
DISCLAIMER: 'disclaimer',
|
||||
TREASURY: 'treasury',
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ import { PartyAccountsByAsset } from './parties/id/accounts';
|
||||
import { Disclaimer } from './pages/disclaimer';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import RestrictedPage from './restricted';
|
||||
import { NetworkTreasury } from './treasury';
|
||||
|
||||
export type Navigable = {
|
||||
path: string;
|
||||
@@ -229,6 +230,17 @@ export const useRouterConfig = () => {
|
||||
]
|
||||
: [];
|
||||
|
||||
const treasuryRoutes: Route[] = [
|
||||
{
|
||||
path: Routes.TREASURY,
|
||||
handle: {
|
||||
name: t('Treasury'),
|
||||
text: t('Treasury'),
|
||||
breadcrumb: () => <Link to={Routes.TREASURY}>{t('Treasury')}</Link>,
|
||||
},
|
||||
element: <NetworkTreasury />,
|
||||
},
|
||||
];
|
||||
const validators: Route[] = featureFlags.EXPLORER_VALIDATORS
|
||||
? [
|
||||
{
|
||||
@@ -358,6 +370,7 @@ export const useRouterConfig = () => {
|
||||
...marketsRoutes,
|
||||
...networkParametersRoutes,
|
||||
...validators,
|
||||
...treasuryRoutes,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
query ExplorerTreasury {
|
||||
assetsConnection(pagination: { last: 1000 }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
networkTreasuryAccount {
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
query ExplorerTreasuryTransfers {
|
||||
transfersConnection(
|
||||
partyId: "network"
|
||||
direction: ToOrFrom
|
||||
pagination: { last: 200 }
|
||||
) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
transfer {
|
||||
timestamp
|
||||
from
|
||||
amount
|
||||
to
|
||||
status
|
||||
reason
|
||||
toAccountType
|
||||
fromAccountType
|
||||
asset {
|
||||
id
|
||||
}
|
||||
id
|
||||
status
|
||||
kind {
|
||||
... on OneOffTransfer {
|
||||
deliverOn
|
||||
}
|
||||
... on RecurringTransfer {
|
||||
startEpoch
|
||||
}
|
||||
... on OneOffGovernanceTransfer {
|
||||
deliverOn
|
||||
}
|
||||
... on RecurringGovernanceTransfer {
|
||||
endEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerTreasuryQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerTreasuryQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, networkTreasuryAccount?: { __typename?: 'AccountBalance', balance: string } | null } } | null> | null } | null };
|
||||
|
||||
|
||||
export const ExplorerTreasuryDocument = gql`
|
||||
query ExplorerTreasury {
|
||||
assetsConnection(pagination: {last: 1000}) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
networkTreasuryAccount {
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerTreasuryQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerTreasuryQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerTreasuryQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerTreasuryQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerTreasuryQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>(ExplorerTreasuryDocument, options);
|
||||
}
|
||||
export function useExplorerTreasuryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>(ExplorerTreasuryDocument, options);
|
||||
}
|
||||
export type ExplorerTreasuryQueryHookResult = ReturnType<typeof useExplorerTreasuryQuery>;
|
||||
export type ExplorerTreasuryLazyQueryHookResult = ReturnType<typeof useExplorerTreasuryLazyQuery>;
|
||||
export type ExplorerTreasuryQueryResult = Apollo.QueryResult<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>;
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerTreasuryTransfersQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerTreasuryTransfersQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', timestamp: any, from: string, amount: string, to: string, status: Types.TransferStatus, reason?: string | null, toAccountType: Types.AccountType, fromAccountType: Types.AccountType, id: string, asset?: { __typename?: 'Asset', id: string } | null, kind: { __typename?: 'OneOffGovernanceTransfer', deliverOn?: any | null } | { __typename?: 'OneOffTransfer', deliverOn?: any | null } | { __typename?: 'RecurringGovernanceTransfer', endEpoch?: number | null } | { __typename?: 'RecurringTransfer', startEpoch: number } } } } | null> | null } | null };
|
||||
|
||||
|
||||
export const ExplorerTreasuryTransfersDocument = gql`
|
||||
query ExplorerTreasuryTransfers {
|
||||
transfersConnection(
|
||||
partyId: "network"
|
||||
direction: ToOrFrom
|
||||
pagination: {last: 200}
|
||||
) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
transfer {
|
||||
timestamp
|
||||
from
|
||||
amount
|
||||
to
|
||||
status
|
||||
reason
|
||||
toAccountType
|
||||
fromAccountType
|
||||
asset {
|
||||
id
|
||||
}
|
||||
id
|
||||
status
|
||||
kind {
|
||||
... on OneOffTransfer {
|
||||
deliverOn
|
||||
}
|
||||
... on RecurringTransfer {
|
||||
startEpoch
|
||||
}
|
||||
... on OneOffGovernanceTransfer {
|
||||
deliverOn
|
||||
}
|
||||
... on RecurringGovernanceTransfer {
|
||||
endEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerTreasuryTransfersQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerTreasuryTransfersQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerTreasuryTransfersQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerTreasuryTransfersQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerTreasuryTransfersQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>(ExplorerTreasuryTransfersDocument, options);
|
||||
}
|
||||
export function useExplorerTreasuryTransfersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>(ExplorerTreasuryTransfersDocument, options);
|
||||
}
|
||||
export type ExplorerTreasuryTransfersQueryHookResult = ReturnType<typeof useExplorerTreasuryTransfersQuery>;
|
||||
export type ExplorerTreasuryTransfersLazyQueryHookResult = ReturnType<typeof useExplorerTreasuryTransfersLazyQuery>;
|
||||
export type ExplorerTreasuryTransfersQueryResult = Apollo.QueryResult<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>;
|
||||
@@ -0,0 +1,37 @@
|
||||
// NOTE: These are a temporary measure, pulled from an old branch on console.
|
||||
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { USDc } from './usdc';
|
||||
import { Vega } from './vega';
|
||||
import { USDt } from './usdt';
|
||||
|
||||
export interface AssetIconProps {
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A poorly implemented, limited support for asset icons.
|
||||
*
|
||||
* These are committed as 'deprecated' to discourage use outside the Treasury page. Rather
|
||||
* than use this, a better approach would be to use source contract addresses to match assets.
|
||||
* This will be done separately.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export function AssetIcon({ symbol }: AssetIconProps) {
|
||||
const s = symbol.toLowerCase();
|
||||
switch (s) {
|
||||
case 'a4a16e250a09a86061ec83c2f9466fc9dc33d332f86876ee74b6f128a5cd6710': // mainnet
|
||||
case 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d': // mainnet
|
||||
return <USDc size={32} />;
|
||||
case 'd1984e3d365faa05bcafbe41f50f90e3663ee7c0da22bb1e24b164e9532691b2': // mainnet
|
||||
case 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55': // testnet
|
||||
return <Vega size={32} />;
|
||||
case 'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba': // mainnet
|
||||
case 'ede4076aef07fd79502d14326c54ab3911558371baaf697a19d077f4f89de399': // testnet
|
||||
return <USDt size={32} />;
|
||||
default:
|
||||
return <Icon name={IconNames.BANK_ACCOUNT} size={8} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* See note in index.tsx. This component is intended as a placeholder for a
|
||||
* better, more generic solution.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const USDc = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 2000 2000">
|
||||
<path
|
||||
d="M1000 2000c554.17 0 1000-445.83 1000-1000S1554.17 0 1000 0 0 445.83 0 1000s445.83 1000 1000 1000z"
|
||||
fill="#2775ca"
|
||||
/>
|
||||
<path
|
||||
d="M1275 1158.33c0-145.83-87.5-195.83-262.5-216.66-125-16.67-150-50-150-108.34s41.67-95.83 125-95.83c75 0 116.67 25 137.5 87.5 4.17 12.5 16.67 20.83 29.17 20.83h66.66c16.67 0 29.17-12.5 29.17-29.16v-4.17c-16.67-91.67-91.67-162.5-187.5-170.83v-100c0-16.67-12.5-29.17-33.33-33.34h-62.5c-16.67 0-29.17 12.5-33.34 33.34v95.83c-125 16.67-204.16 100-204.16 204.17 0 137.5 83.33 191.66 258.33 212.5 116.67 20.83 154.17 45.83 154.17 112.5s-58.34 112.5-137.5 112.5c-108.34 0-145.84-45.84-158.34-108.34-4.16-16.66-16.66-25-29.16-25h-70.84c-16.66 0-29.16 12.5-29.16 29.17v4.17c16.66 104.16 83.33 179.16 220.83 200v100c0 16.66 12.5 29.16 33.33 33.33h62.5c16.67 0 29.17-12.5 33.34-33.33v-100c125-20.84 208.33-108.34 208.33-220.84z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M787.5 1595.83c-325-116.66-491.67-479.16-370.83-800 62.5-175 200-308.33 370.83-370.83 16.67-8.33 25-20.83 25-41.67V325c0-16.67-8.33-29.17-25-33.33-4.17 0-12.5 0-16.67 4.16-395.83 125-612.5 545.84-487.5 941.67 75 233.33 254.17 412.5 487.5 487.5 16.67 8.33 33.34 0 37.5-16.67 4.17-4.16 4.17-8.33 4.17-16.66v-58.34c0-12.5-12.5-29.16-25-37.5zM1229.17 295.83c-16.67-8.33-33.34 0-37.5 16.67-4.17 4.17-4.17 8.33-4.17 16.67v58.33c0 16.67 12.5 33.33 25 41.67 325 116.66 491.67 479.16 370.83 800-62.5 175-200 308.33-370.83 370.83-16.67 8.33-25 20.83-25 41.67V1700c0 16.67 8.33 29.17 25 33.33 4.17 0 12.5 0 16.67-4.16 395.83-125 612.5-545.84 487.5-941.67-75-237.5-258.34-416.67-487.5-491.67z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* See note in index.tsx. This component is intended as a placeholder for a
|
||||
* better, more generic solution.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const USDt = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 339.43 295.27">
|
||||
<path
|
||||
fill="#50af95"
|
||||
d="M62.15,1.45l-61.89,130a2.52,2.52,0,0,0,.54,2.94L167.95,294.56a2.55,2.55,0,0,0,3.53,0L338.63,134.4a2.52,2.52,0,0,0,.54-2.94l-61.89-130A2.5,2.5,0,0,0,275,0H64.45a2.5,2.5,0,0,0-2.3,1.45h0Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M191.19,144.8v0c-1.2.09-7.4,0.46-21.23,0.46-11,0-18.81-.33-21.55-0.46v0c-42.51-1.87-74.24-9.27-74.24-18.13s31.73-16.25,74.24-18.15v28.91c2.78,0.2,10.74.67,21.74,0.67,13.2,0,19.81-.55,21-0.66v-28.9c42.42,1.89,74.08,9.29,74.08,18.13s-31.65,16.24-74.08,18.12h0Zm0-39.25V79.68h59.2V40.23H89.21V79.68H148.4v25.86c-48.11,2.21-84.29,11.74-84.29,23.16s36.18,20.94,84.29,23.16v82.9h42.78V151.83c48-2.21,84.12-11.73,84.12-23.14s-36.09-20.93-84.12-23.15h0Zm0,0h0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* See note in index.tsx. This component is intended as a placeholder for a
|
||||
* better, more generic solution.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const Vega = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 42 42">
|
||||
<rect width="42" height="42" rx="21" fill="black" />
|
||||
<path d="M13 27.2726H16.4545V10H13V27.2726Z" fill="white" />
|
||||
<path d="M25.667 23.8181H29.1215V10H25.667V23.8181Z" fill="white" />
|
||||
<path d="M19.333 33.6059H22.7875V30.1514H19.333V33.6059Z" fill="white" />
|
||||
<path
|
||||
d="M22.7871 30.7271H26.2416V27.2726H22.7871V30.7271Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M29.1211 27.2726H31.9999V23.8181H29.1211V27.2726Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M16.4551 30.7271H19.3339V27.2726H16.4551V30.7271Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import type { DeepPartial } from '@apollo/client/utilities';
|
||||
import { parseResultsToAccounts } from './network-accounts-table';
|
||||
import {
|
||||
ExplorerTreasuryDocument,
|
||||
type ExplorerTreasuryQuery,
|
||||
} from '../__generated__/Treasury';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { NetworkAccountsTable } from './network-accounts-table';
|
||||
|
||||
describe('parseResultsToAccounts', () => {
|
||||
it('should return an array of non-zero treasury accounts', () => {
|
||||
const data: DeepPartial<ExplorerTreasuryQuery> = {
|
||||
assetsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'asset1',
|
||||
networkTreasuryAccount: {
|
||||
balance: '100',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'has0assets',
|
||||
networkTreasuryAccount: {
|
||||
balance: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'asset3',
|
||||
networkTreasuryAccount: {
|
||||
balance: '50',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'hasnonetworktreasuryaccount',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseResultsToAccounts(data as ExplorerTreasuryQuery);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
assetId: 'asset1',
|
||||
balance: '100',
|
||||
type: 'ACCOUNT_TYPE_NETWORK_TREASURY',
|
||||
},
|
||||
{
|
||||
assetId: 'asset3',
|
||||
balance: '50',
|
||||
type: 'ACCOUNT_TYPE_NETWORK_TREASURY',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array if no non-zero accounts are found', () => {
|
||||
const data: DeepPartial<ExplorerTreasuryQuery> = {
|
||||
assetsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'asset1',
|
||||
networkTreasuryAccount: {
|
||||
balance: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'asset2',
|
||||
networkTreasuryAccount: {
|
||||
balance: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseResultsToAccounts(data as ExplorerTreasuryQuery);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle missing data', () => {
|
||||
const result = parseResultsToAccounts(
|
||||
undefined as unknown as ExplorerTreasuryQuery
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NetworkAccountsTable', () => {
|
||||
const mockData: ExplorerTreasuryQuery = {
|
||||
assetsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'asset1',
|
||||
networkTreasuryAccount: {
|
||||
balance: '100',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'asset2',
|
||||
networkTreasuryAccount: {
|
||||
balance: '50',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: ExplorerTreasuryDocument,
|
||||
},
|
||||
result: {
|
||||
data: mockData,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('should render network accounts (as many as match - often just 1)', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<MemoryRouter>
|
||||
<NetworkAccountsTable />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
// Wait for the data to load
|
||||
await screen.findByText('Loading...');
|
||||
|
||||
// Assert that the network accounts are rendered
|
||||
expect(screen.getByText('asset1')).toBeInTheDocument();
|
||||
expect(screen.getByText('asset2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle loading state', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<MemoryRouter>
|
||||
<NetworkAccountsTable />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
// Assert that the loading state is rendered
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
type ExplorerTreasuryQuery,
|
||||
useExplorerTreasuryQuery,
|
||||
} from '../__generated__/Treasury';
|
||||
import AssetBalance from '../../../components/asset-balance/asset-balance';
|
||||
import { AssetLink } from '../../../components/links';
|
||||
import { useMemo } from 'react';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { AssetIcon } from './asset-icon';
|
||||
import { type NonZeroAccount } from '../network-treasury';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
|
||||
export const NetworkAccountsTable = () => {
|
||||
const { data, loading, error } = useExplorerTreasuryQuery({
|
||||
// This needs to ignore error as old assets may no longer properly resolve
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const shouldRound = useMemo(
|
||||
() => ['xs', 'sm', 'md', 'lg'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
render={(data) => {
|
||||
const c = parseResultsToAccounts(data);
|
||||
return (
|
||||
<section className="md:flex md:flex-row flex-wrap">
|
||||
{c.map((a) => (
|
||||
<div className="basis-1/2 md:basis-1/4">
|
||||
<div className="bg-white rounded overflow-hidden shadow-lg dark:bg-black dark:border-slate-500 dark:border">
|
||||
<div className="text-center p-6 bg-gray-100 dark:bg-slate-900 border-b dark:border-slate-500">
|
||||
<p className="flex justify-center">
|
||||
<AssetIcon symbol={a.assetId} />
|
||||
</p>
|
||||
<p className="mt-3" data-testid="name">
|
||||
<AssetLink assetId={a.assetId} />
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center py-5" data-testid="balance">
|
||||
<AssetBalance
|
||||
assetId={a.assetId}
|
||||
price={a.balance}
|
||||
showAssetSymbol={true}
|
||||
rounded={shouldRound}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function parseResultsToAccounts(
|
||||
data: ExplorerTreasuryQuery
|
||||
): NonZeroAccount[] {
|
||||
const nonZeroAccounts: NonZeroAccount[] = [];
|
||||
if (data?.assetsConnection?.edges) {
|
||||
const edges = removePaginationWrapper(data?.assetsConnection?.edges);
|
||||
if (edges) {
|
||||
edges.forEach((edge) => {
|
||||
if (
|
||||
edge.networkTreasuryAccount &&
|
||||
edge.networkTreasuryAccount?.balance !== '0'
|
||||
) {
|
||||
nonZeroAccounts.push({
|
||||
assetId: edge.id,
|
||||
balance: edge.networkTreasuryAccount?.balance,
|
||||
type: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return nonZeroAccounts;
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import {
|
||||
typeLabel,
|
||||
getToAccountTypeLabel,
|
||||
filterAccountTransfers,
|
||||
} from './network-transfers-table';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { NetworkTransfersTable } from './network-transfers-table';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
ExplorerTreasuryTransfersDocument,
|
||||
type ExplorerTreasuryTransfersQuery,
|
||||
} from '../__generated__/TreasuryTransfers';
|
||||
import type { DeepPartial } from '@apollo/client/utilities';
|
||||
|
||||
describe('typeLabel', () => {
|
||||
it('should return "Transfer" for "OneOffTransfer" kind', () => {
|
||||
expect(typeLabel('OneOffTransfer')).toBe('Transfer');
|
||||
});
|
||||
|
||||
it('should return "Transfer" for "RecurringTransfer" kind', () => {
|
||||
expect(typeLabel('RecurringTransfer')).toBe('Transfer');
|
||||
});
|
||||
|
||||
it('should return "Governance" for "OneOffGovernanceTransfer" kind', () => {
|
||||
expect(typeLabel('OneOffGovernanceTransfer')).toBe('Governance');
|
||||
});
|
||||
|
||||
it('should return "Governance" for "RecurringGovernanceTransfer" kind', () => {
|
||||
expect(typeLabel('RecurringGovernanceTransfer')).toBe('Governance');
|
||||
});
|
||||
|
||||
it('should return "Unknown" for unknown kind', () => {
|
||||
expect(typeLabel()).toBe('Unknown');
|
||||
expect(typeLabel('')).toBe('Unknown');
|
||||
expect(typeLabel('InvalidKind')).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToAccountTypeLabel', () => {
|
||||
it('should return "Treasury" when type is ACCOUNT_TYPE_NETWORK_TREASURY', () => {
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_NETWORK_TREASURY)
|
||||
).toBe('Treasury');
|
||||
});
|
||||
|
||||
it('should return "Fees" when type is any of the fee account types', () => {
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE)
|
||||
).toBe('Fees');
|
||||
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_MAKER)).toBe(
|
||||
'Fees'
|
||||
);
|
||||
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY)).toBe(
|
||||
'Fees'
|
||||
);
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES)
|
||||
).toBe('Fees');
|
||||
expect(
|
||||
getToAccountTypeLabel(
|
||||
AccountType.ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD
|
||||
)
|
||||
).toBe('Fees');
|
||||
});
|
||||
|
||||
it('should return "Insurance" when type is ACCOUNT_TYPE_GLOBAL_INSURANCE', () => {
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_GLOBAL_INSURANCE)
|
||||
).toBe('Insurance');
|
||||
});
|
||||
|
||||
it('should return "Rewards" when type is any of the reward account types', () => {
|
||||
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD)).toBe(
|
||||
'Rewards'
|
||||
);
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION)
|
||||
).toBe('Rewards');
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES)
|
||||
).toBe('Rewards');
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES)
|
||||
).toBe('Rewards');
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES)
|
||||
).toBe('Rewards');
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS)
|
||||
).toBe('Rewards');
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN)
|
||||
).toBe('Rewards');
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY)
|
||||
).toBe('Rewards');
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING)
|
||||
).toBe('Rewards');
|
||||
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_VESTED_REWARDS)).toBe(
|
||||
'Rewards'
|
||||
);
|
||||
expect(
|
||||
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_VESTING_REWARDS)
|
||||
).toBe('Rewards');
|
||||
});
|
||||
|
||||
it('should return "Other" for any other type', () => {
|
||||
expect(getToAccountTypeLabel(undefined)).toBe('Other');
|
||||
expect(getToAccountTypeLabel('unknown' as AccountType)).toBe('Other');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterAccountTransfers', () => {
|
||||
it('filters out transactions that are not to or from a treasury account', () => {
|
||||
const data: DeepPartial<ExplorerTreasuryTransfersQuery> = {
|
||||
transfersConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
transfer: {
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
transfer: {
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
|
||||
fromAccountType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
transfer: {
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
transfer: {
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
fromAccountType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterAccountTransfers(
|
||||
data as ExplorerTreasuryTransfersQuery
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should return an empty array if no transfers match the filter', () => {
|
||||
const data: DeepPartial<ExplorerTreasuryTransfersQuery> = {
|
||||
transfersConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
transfer: {
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
transfer: {
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
fromAccountType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterAccountTransfers(
|
||||
data as ExplorerTreasuryTransfersQuery
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NetworkTransfersTable', () => {
|
||||
it('renders table headers correctly', async () => {
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: ExplorerTreasuryTransfersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
transfersConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
transfer: {
|
||||
id: '123',
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
|
||||
fromAccountType:
|
||||
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
|
||||
amount: '100',
|
||||
asset: {
|
||||
id: '1',
|
||||
},
|
||||
timestamp: '2022-01-01T00:00:00Z',
|
||||
from: 'network',
|
||||
to: '7100a8a82ef45adb9efa070cc821c6c5c48172d6dc5f842431549490fe5897a0',
|
||||
reason: '',
|
||||
status: 'COMPLETED',
|
||||
kind: {
|
||||
__typename: 'OneOffGovernanceTransfer',
|
||||
deliverOn: '123',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={mocks} addTypename={true}>
|
||||
<MemoryRouter>
|
||||
<NetworkTransfersTable />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Amount')).toBeInTheDocument();
|
||||
expect(screen.getByText('Asset')).toBeInTheDocument();
|
||||
expect(screen.getByText('Age')).toBeInTheDocument();
|
||||
expect(screen.getByText('From')).toBeInTheDocument();
|
||||
expect(screen.getByText('To')).toBeInTheDocument();
|
||||
expect(screen.getByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getByText('Type')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('from-account').textContent).toEqual('Treasury');
|
||||
expect(screen.getByTestId('to-account').textContent).toEqual('7100…97a0');
|
||||
expect(screen.getByTestId('transfer-kind').textContent).toEqual(
|
||||
'Governance'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
import { AsyncRenderer, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import AssetBalance from '../../../components/asset-balance/asset-balance';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { AssetLink, PartyLink } from '../../../components/links';
|
||||
import {
|
||||
type ExplorerTreasuryTransfersQuery,
|
||||
useExplorerTreasuryTransfersQuery,
|
||||
} from '../__generated__/TreasuryTransfers';
|
||||
import { TimeAgo } from '../../../components/time-ago';
|
||||
import { TransferStatusIcon } from '../../../components/txs/details/transfer/blocks/transfer-status';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { useMemo } from 'react';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const colours = {
|
||||
INCOMING: '!fill-vega-green-600 text-vega-green-600 mr-2',
|
||||
OUTGOING: '!fill-vega-pink-600 text-vega-pink-600 mr-2',
|
||||
};
|
||||
|
||||
export const theadClasses =
|
||||
'py-2 border text-center bg-vega-light-150 dark:bg-vega-dark-150';
|
||||
|
||||
export function getToAccountTypeLabel(type?: AccountType): string {
|
||||
switch (type) {
|
||||
case AccountType.ACCOUNT_TYPE_NETWORK_TREASURY:
|
||||
return t('Treasury');
|
||||
case AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE:
|
||||
case AccountType.ACCOUNT_TYPE_FEES_MAKER:
|
||||
case AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY:
|
||||
case AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES:
|
||||
case AccountType.ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD:
|
||||
return t('Fees');
|
||||
case AccountType.ACCOUNT_TYPE_GLOBAL_INSURANCE:
|
||||
return t('Insurance');
|
||||
case AccountType.ACCOUNT_TYPE_GLOBAL_REWARD:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY:
|
||||
case AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING:
|
||||
case AccountType.ACCOUNT_TYPE_VESTED_REWARDS:
|
||||
case AccountType.ACCOUNT_TYPE_VESTING_REWARDS:
|
||||
return t('Rewards');
|
||||
default:
|
||||
return t('Other');
|
||||
}
|
||||
}
|
||||
|
||||
export function typeLabel(kind?: string): string {
|
||||
switch (kind) {
|
||||
case 'OneOffTransfer':
|
||||
case 'RecurringTransfer':
|
||||
return t('Transfer');
|
||||
case 'OneOffGovernanceTransfer':
|
||||
case 'RecurringGovernanceTransfer':
|
||||
return t('Governance');
|
||||
default:
|
||||
return t('Unknown');
|
||||
}
|
||||
}
|
||||
|
||||
export function filterAccountTransfers(data: ExplorerTreasuryTransfersQuery) {
|
||||
return data.transfersConnection?.edges
|
||||
?.filter((edge) => {
|
||||
if (
|
||||
edge?.node.transfer.toAccountType ===
|
||||
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY
|
||||
) {
|
||||
return true;
|
||||
} else if (
|
||||
edge?.node.transfer.fromAccountType ===
|
||||
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((edge) => {
|
||||
return edge?.node.transfer;
|
||||
});
|
||||
}
|
||||
|
||||
export const NetworkTransfersTable = () => {
|
||||
const { data, loading, error } = useExplorerTreasuryTransfersQuery({
|
||||
// This needs to ignore error as old assets may no longer properly resolve
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const shouldRound = useMemo(
|
||||
() => ['xs', 'sm', 'md', 'lg'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
const shouldTruncate = useMemo(
|
||||
() => ['xs', 'sm', 'md', 'lg', 'xl'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
const shouldHideColumns = useMemo(
|
||||
() => ['xs', 'sm'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
render={(data) => {
|
||||
const c = filterAccountTransfers(data);
|
||||
if (!c) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<table className="table-fixed border-spacing-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={theadClasses}>{t('Amount')}</th>
|
||||
<th className={theadClasses}>{t('Asset')}</th>
|
||||
<th className={theadClasses}>{t('Age')}</th>
|
||||
<th className={theadClasses}>{t('From')}</th>
|
||||
<th className={theadClasses}>{t('To')}</th>
|
||||
<th
|
||||
className={`${theadClasses} ${
|
||||
shouldHideColumns ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
{t('Status')}
|
||||
</th>
|
||||
<th
|
||||
className={`${theadClasses} ${
|
||||
shouldHideColumns ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
{t('Type')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{c.map((a) => {
|
||||
const isIncoming =
|
||||
a?.toAccountType ===
|
||||
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY;
|
||||
return (
|
||||
<tr>
|
||||
{a && a.amount && a.asset && (
|
||||
<td
|
||||
className={`px-2 py-1 border whitespace-nowrap text-right ${
|
||||
isIncoming ? colours.INCOMING : colours.OUTGOING
|
||||
}`}
|
||||
title={a.amount}
|
||||
>
|
||||
{a &&
|
||||
a.toAccountType ===
|
||||
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY ? (
|
||||
<Icon
|
||||
name={IconNames.PLUS}
|
||||
className={colours.INCOMING}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name={IconNames.MINUS}
|
||||
className={colours.OUTGOING}
|
||||
/>
|
||||
)}
|
||||
<AssetBalance
|
||||
assetId={a.asset.id}
|
||||
price={a.amount}
|
||||
showAssetLink={false}
|
||||
rounded={shouldRound}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-2 py-1 border whitespace-nowrap">
|
||||
{a && a.amount && a.asset && (
|
||||
<AssetLink
|
||||
assetId={a.asset.id}
|
||||
showAssetSymbol={true}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-1 border">
|
||||
{a && a.timestamp && <TimeAgo date={a.timestamp} />}
|
||||
</td>
|
||||
<td
|
||||
className="px-2 py-1 border"
|
||||
data-testid="from-account"
|
||||
>
|
||||
{a && a.from && (
|
||||
<PartyLink
|
||||
id={a.from}
|
||||
truncate={true}
|
||||
truncateLength={shouldTruncate ? 4 : 15}
|
||||
networkLabel={t('Treasury')}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-1 border" data-testid="to-account">
|
||||
{a && a.to && (
|
||||
<PartyLink
|
||||
id={a.to}
|
||||
networkLabel={t('Treasury')}
|
||||
truncate={true}
|
||||
truncateLength={shouldTruncate ? 4 : 15}
|
||||
/>
|
||||
)}
|
||||
{a && !a.to && (
|
||||
<span
|
||||
className="underline decoration-dotted"
|
||||
title={AccountTypeMapping[a.toAccountType]}
|
||||
>
|
||||
{getToAccountTypeLabel(a.toAccountType)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
className={`px-2 py-1 border text-center ${
|
||||
shouldHideColumns ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
{a && a.status && (
|
||||
<TransferStatusIcon status={a.status} />
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
className={`px-2 py-1 border ${
|
||||
shouldHideColumns ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="underline decoration-dotted"
|
||||
title={a?.kind.__typename}
|
||||
data-testid="transfer-kind"
|
||||
>
|
||||
{a && typeLabel(a.kind.__typename)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './network-treasury';
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import type { AccountType } from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { NetworkAccountsTable } from './components/network-accounts-table';
|
||||
import { NetworkTransfersTable } from './components/network-transfers-table';
|
||||
|
||||
export type NonZeroAccount = {
|
||||
assetId: string;
|
||||
balance: string;
|
||||
type: AccountType;
|
||||
};
|
||||
|
||||
export const NetworkTreasury = () => {
|
||||
useDocumentTitle(['Network Treasury']);
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="block-header">{t(`Treasury`)}</RouteTitle>
|
||||
<div>
|
||||
<NetworkAccountsTable />
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h2 className="text-3xl mb-2">{t('Transfers')}</h2>
|
||||
<NetworkTransfersTable />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ export const proposalsData = {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: 'e8ba9d268e12514644fd1fc7ff289292f4ce6489cc32cc73133aea52c04aef89',
|
||||
rationale: {
|
||||
title: 'Add asset Wrapped Ether',
|
||||
@@ -56,7 +56,7 @@ export const proposalsData = {
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: 'd848fc7881f13d366df5f61ab139d5fcfa72bf838151bb51b54381870e357931',
|
||||
rationale: {
|
||||
title: 'Add asset Dai Stablecoin',
|
||||
@@ -110,60 +110,7 @@ export const proposalsData = {
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'ccbd651b4a1167fd73c4a0340ac759fa0a31ca487ad46a13254b741ad71947ed',
|
||||
rationale: {
|
||||
title: 'New DAI market',
|
||||
description: 'New DAI market',
|
||||
__typename: 'ProposalRationale',
|
||||
},
|
||||
reference: '0VFQusmmESdrP5GuL8naB6lxfoE3RPGaEeo7abdN',
|
||||
state: 'STATE_ENACTED',
|
||||
datetime: '2022-11-26T19:36:19.26034Z',
|
||||
rejectionReason: null,
|
||||
party: {
|
||||
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
||||
__typename: 'Party',
|
||||
},
|
||||
errorDetails: null,
|
||||
terms: {
|
||||
closingDatetime: '2022-11-26T19:36:42Z',
|
||||
enactmentDatetime: '2023-03-22T13:57:37Z',
|
||||
change: {
|
||||
instrument: {
|
||||
name: 'UNIDAI Monthly (Dec 2022)',
|
||||
code: 'UNIDAI.MF21',
|
||||
product: {
|
||||
settlementAsset: { symbol: 'tDAI', __typename: 'Asset' },
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
__typename: 'InstrumentConfiguration',
|
||||
},
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
__typename: 'ProposalTerms',
|
||||
},
|
||||
votes: {
|
||||
yes: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
no: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
__typename: 'ProposalVotes',
|
||||
},
|
||||
__typename: 'Proposal',
|
||||
},
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: 'bc70383f0e9515b15542cf4c63590cd2ca46b3363ba7c4a72af0e62112b3951b',
|
||||
rationale: {
|
||||
title: 'USDC-III',
|
||||
@@ -217,60 +164,7 @@ export const proposalsData = {
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '9d9b2a9d0179d0e4ccb317f6c4a5db0b905d893190bfb5e5499985ef313281c8',
|
||||
rationale: {
|
||||
title: 'New BTC market',
|
||||
description: 'New BTC market',
|
||||
__typename: 'ProposalRationale',
|
||||
},
|
||||
reference: 'AXeRWS3TvLBFDgWOSHQpKFJf3NTbnWK6310q02fZ',
|
||||
state: 'STATE_ENACTED',
|
||||
datetime: '2022-11-26T19:36:19.26034Z',
|
||||
rejectionReason: null,
|
||||
party: {
|
||||
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
||||
__typename: 'Party',
|
||||
},
|
||||
errorDetails: null,
|
||||
terms: {
|
||||
closingDatetime: '2022-11-26T19:36:42Z',
|
||||
enactmentDatetime: '2023-03-22T13:57:37Z',
|
||||
change: {
|
||||
instrument: {
|
||||
name: 'ETHBTC Quarterly (Feb 2023)',
|
||||
code: 'ETHBTC.QM21',
|
||||
product: {
|
||||
settlementAsset: { symbol: 'tBTC', __typename: 'Asset' },
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
__typename: 'InstrumentConfiguration',
|
||||
},
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
__typename: 'ProposalTerms',
|
||||
},
|
||||
votes: {
|
||||
yes: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
no: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
__typename: 'ProposalVotes',
|
||||
},
|
||||
__typename: 'Proposal',
|
||||
},
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: '9c48796e7988769ededc2b2b02220b00e93f65f23e8141bf1fd23a6983d95943',
|
||||
rationale: {
|
||||
title: 'Update governance.proposal.asset.requiredMajority',
|
||||
|
||||
@@ -236,7 +236,7 @@ context(
|
||||
});
|
||||
|
||||
// 1002-STKE-041 1002-STKE-053
|
||||
it(
|
||||
it.skip(
|
||||
'Able to remove part of a stake against a validator',
|
||||
// @ts-ignore clash between jest and cypress
|
||||
{ tags: '@smoke' },
|
||||
|
||||
@@ -27,7 +27,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.getByTestId('app-announcement').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show open or enacted proposals without proposal summary', function () {
|
||||
it.skip('should show open or enacted proposals without proposal summary', function () {
|
||||
cy.get('body').then(($body) => {
|
||||
if (!$body.find('[data-testid="proposals-list-item"]').length) {
|
||||
cy.createMarket();
|
||||
@@ -82,20 +82,20 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
it('should have information on active nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.first()
|
||||
.should('contain.text', '2')
|
||||
.should('contain.text', '1')
|
||||
.and('contain.text', 'active nodes');
|
||||
});
|
||||
|
||||
it('should have information on consensus nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.last()
|
||||
.should('contain.text', '2')
|
||||
.should('contain.text', '1')
|
||||
.and('contain.text', 'consensus nodes');
|
||||
});
|
||||
|
||||
it('should contain link to specific validators', function () {
|
||||
cy.getByTestId('validators')
|
||||
.should('have.length', '2')
|
||||
.should('have.length', '1')
|
||||
.each(($validator) => {
|
||||
cy.wrap($validator).find('a').should('have.attr', 'href');
|
||||
});
|
||||
@@ -159,7 +159,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.getByTestId('node-url-custom').click({ force: true });
|
||||
cy.get('input').should('exist');
|
||||
cy.getByTestId('connect').should('be.disabled');
|
||||
cy.getByTestId('icon-cross').click();
|
||||
cy.getByTestId('dialog-close').click();
|
||||
});
|
||||
|
||||
it('should display eth data', function () {
|
||||
@@ -189,7 +189,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.viewport('iphone-xr');
|
||||
});
|
||||
|
||||
it('should have burger button', () => {
|
||||
it.skip('should have burger button', () => {
|
||||
cy.getByTestId('button-menu-drawer').should('be.visible').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
});
|
||||
|
||||
@@ -40,10 +40,10 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
|
||||
verifyConnectedToPubKey();
|
||||
});
|
||||
|
||||
it('Able to connect public key via wallet and view assets in wallet', function () {
|
||||
it.skip('Able to connect public key via wallet and view assets in wallet', function () {
|
||||
verifyConnectedToPubKey();
|
||||
cy.getByTestId('currency-title', { timeout: 10000 })
|
||||
.should('have.length.at.least', 4)
|
||||
.should('have.length.at.least', 2)
|
||||
.and('contain.text', 'USDC (fake)');
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
|
||||
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
@@ -24,7 +23,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
@@ -33,7 +32,7 @@ LC_ALL="en_US.UTF-8"
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=false
|
||||
NX_UPDATE_MARKET_STATE=false
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
NX_GOVERNANCE_TRANSFERS=false
|
||||
NX_GOVERNANCE_TRANSFERS=true
|
||||
|
||||
@@ -15,12 +15,11 @@ NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit suppl
|
||||
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
|
||||
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
|
||||
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=#
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
||||
|
||||
@@ -10,7 +10,6 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
|
||||
|
||||
@@ -9,7 +9,6 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
|
||||
|
||||
@@ -5,7 +5,6 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","STAGNET1":"https:
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
|
||||
@@ -13,7 +12,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
|
||||
@@ -9,7 +9,6 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
|
||||
@@ -18,7 +17,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
@@ -14,7 +14,7 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
|
||||
|
||||
@@ -62,6 +62,9 @@ const cache: InMemoryCacheConfig = {
|
||||
Account: {
|
||||
keyFields: false,
|
||||
},
|
||||
Instrument: {
|
||||
keyFields: ['code'],
|
||||
},
|
||||
Delegation: {
|
||||
keyFields: false,
|
||||
// Only get full updates
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classnames from 'classnames';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Dispatch, SetStateAction, ReactNode } from 'react';
|
||||
|
||||
interface CollapsibleToggleProps {
|
||||
@@ -15,22 +15,19 @@ export const CollapsibleToggle = ({
|
||||
dataTestId,
|
||||
children,
|
||||
}: CollapsibleToggleProps) => {
|
||||
const classes = classnames(
|
||||
'mb-4 transition-transform ease-in-out duration-300',
|
||||
{
|
||||
'rotate-180': toggleState,
|
||||
}
|
||||
);
|
||||
const classes = classnames('transition-transform ease-in-out duration-300', {
|
||||
'rotate-180': toggleState,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setToggleState(!toggleState)}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-baseline gap-3">
|
||||
{children}
|
||||
<div className={classes} data-testid="toggle-icon-wrapper">
|
||||
<Icon name="chevron-down" size={8} />
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface HeadingProps {
|
||||
title?: string;
|
||||
title?: ReactNode;
|
||||
centerContent?: boolean;
|
||||
marginTop?: boolean;
|
||||
marginBottom?: boolean;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
useLinks,
|
||||
DApp,
|
||||
CONSOLE_REWARDS_PAGE,
|
||||
} from '@vegaprotocol/environment';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useMatch } from 'react-router-dom';
|
||||
import Routes from '../../routes/routes';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
const ConsoleRewardsLink = ({ children }: { children: ReactNode }) => {
|
||||
const consoleLink = useLinks(DApp.Console);
|
||||
return (
|
||||
<ExternalLink
|
||||
href={consoleLink(CONSOLE_REWARDS_PAGE)}
|
||||
className="underline inline-flex gap-1 items-center"
|
||||
title="Rewards in Console"
|
||||
>
|
||||
<span>{children}</span>
|
||||
<VegaIcon size={12} name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</ExternalLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const RewardsMovedNotification = () => {
|
||||
const onRewardsPage = useMatch(Routes.REWARDS);
|
||||
if (!onRewardsPage) return null;
|
||||
|
||||
return (
|
||||
<NotificationBanner intent={Intent.Warning}>
|
||||
<Trans
|
||||
i18nKey="rewardsMovedNotification"
|
||||
components={[<ConsoleRewardsLink>Console</ConsoleRewardsLink>]}
|
||||
/>
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ProtocolUpgradeProposalNotification,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import { RewardsMovedNotification } from '../notifications/rewards-moved-notification';
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -45,8 +46,10 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
|
||||
|
||||
const NotificationsContainer = () => {
|
||||
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
|
||||
|
||||
return (
|
||||
<div data-testid="banners">
|
||||
<RewardsMovedNotification />
|
||||
<ProtocolUpgradeProposalNotification
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@ import keyBy from 'lodash/keyBy';
|
||||
import uniq from 'lodash/uniq';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ENV } from '../../config';
|
||||
|
||||
import noIcon from '../../images/token-no-icon.png';
|
||||
import vegaBlack from '../../images/vega_black.png';
|
||||
@@ -37,7 +36,6 @@ export const usePollForDelegations = () => {
|
||||
const { t } = useTranslation();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const client = useApolloClient();
|
||||
const { delegationsPagination } = ENV;
|
||||
const [delegations, setDelegations] = React.useState<
|
||||
WalletDelegationFieldsFragment[]
|
||||
>([]);
|
||||
@@ -68,11 +66,9 @@ export const usePollForDelegations = () => {
|
||||
query: DelegationsDocument,
|
||||
variables: {
|
||||
partyId: pubKey,
|
||||
delegationsPagination: delegationsPagination
|
||||
? {
|
||||
first: Number(delegationsPagination),
|
||||
}
|
||||
: undefined,
|
||||
delegationsPagination: {
|
||||
first: 50,
|
||||
},
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
@@ -236,14 +232,14 @@ export const usePollForDelegations = () => {
|
||||
// will just continue to fail
|
||||
clearInterval(interval);
|
||||
});
|
||||
}, 1000);
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
mounted = false;
|
||||
};
|
||||
}, [delegationsPagination, client, decimals, pubKey, t, vegaToken.address]);
|
||||
}, [client, decimals, pubKey, t, vegaToken.address]);
|
||||
|
||||
return { delegations, currentStakeAvailable, delegatedNodes, accounts };
|
||||
};
|
||||
|
||||
@@ -65,7 +65,6 @@ export const ENV = {
|
||||
docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'),
|
||||
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
|
||||
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
|
||||
delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'),
|
||||
rest: windowOrDefault('NX_VEGA_REST_URL'),
|
||||
addresses:
|
||||
ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks],
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('getMultisigStatus', () => {
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.noNodes,
|
||||
showMultisigStatusError: true,
|
||||
zeroScoreNodes: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +36,7 @@ describe('getMultisigStatus', () => {
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.correct,
|
||||
showMultisigStatusError: false,
|
||||
zeroScoreNodes: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +52,22 @@ describe('getMultisigStatus', () => {
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.nodeNeedsRemoving,
|
||||
showMultisigStatusError: true,
|
||||
zeroScoreNodes: [
|
||||
{
|
||||
id: '1',
|
||||
rewardScore: {
|
||||
multisigScore: '0',
|
||||
},
|
||||
stakedTotal: '1000',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
rewardScore: {
|
||||
multisigScore: '0',
|
||||
},
|
||||
stakedTotal: '1000',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +83,15 @@ describe('getMultisigStatus', () => {
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.nodeNeedsAdding,
|
||||
showMultisigStatusError: true,
|
||||
zeroScoreNodes: [
|
||||
{
|
||||
id: '1',
|
||||
rewardScore: {
|
||||
multisigScore: '0',
|
||||
},
|
||||
stakedTotal: '1000',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
|
||||
import type {
|
||||
PreviousEpochQuery,
|
||||
ValidatorNodeFragment,
|
||||
} from '../routes/staking/__generated__/PreviousEpoch';
|
||||
|
||||
export enum MultisigStatus {
|
||||
'correct' = 'correct',
|
||||
@@ -17,12 +20,15 @@ export const getMultisigStatusInfo = (
|
||||
previousEpochData?.epoch.validatorsConnection?.edges
|
||||
);
|
||||
|
||||
const hasZero = allNodesInPreviousEpoch.some(
|
||||
(node) => Number(node?.rewardScore?.multisigScore) === 0
|
||||
);
|
||||
const hasOne = allNodesInPreviousEpoch.some(
|
||||
(node) => Number(node?.rewardScore?.multisigScore) === 1
|
||||
);
|
||||
const zeroScore = (node: ValidatorNodeFragment) =>
|
||||
Number(node.rewardScore?.multisigScore) === 0;
|
||||
const oneScore = (node: ValidatorNodeFragment) =>
|
||||
Number(node.rewardScore?.multisigScore) === 1;
|
||||
|
||||
const hasZero = allNodesInPreviousEpoch.some(zeroScore);
|
||||
const hasOne = allNodesInPreviousEpoch.some(oneScore);
|
||||
|
||||
const zeroScoreNodes = allNodesInPreviousEpoch.filter(zeroScore);
|
||||
|
||||
if (hasZero && hasOne) {
|
||||
// If any individual node has 0 it means that node is missing from the multisig and needs to be added
|
||||
@@ -38,5 +44,6 @@ export const getMultisigStatusInfo = (
|
||||
return {
|
||||
showMultisigStatusError: status !== MultisigStatus.correct,
|
||||
multisigStatus: status,
|
||||
zeroScoreNodes,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -12,10 +13,10 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
|
||||
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
||||
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
|
||||
import Routes from '../routes';
|
||||
import { ExternalLinks, useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
|
||||
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
|
||||
import { useProposalsQuery } from '../proposals/__generated__/Proposals';
|
||||
import {
|
||||
getNotRejectedProposals,
|
||||
getNotRejectedProtocolUpgradeProposals,
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
orderByUpgradeBlockHeight,
|
||||
} from '../proposals/components/proposals-list/proposals-list';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import { type Proposal } from '../proposals/types';
|
||||
import { type Proposal, type BatchProposal } from '../proposals/types';
|
||||
|
||||
const nodesToShow = 6;
|
||||
|
||||
@@ -39,7 +40,7 @@ const HomeProposals = ({
|
||||
proposals,
|
||||
protocolUpgradeProposals,
|
||||
}: {
|
||||
proposals: Proposal[];
|
||||
proposals: Array<Proposal | BatchProposal>;
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -60,12 +61,9 @@ const HomeProposals = ({
|
||||
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
|
||||
))}
|
||||
|
||||
{proposals.map(
|
||||
(proposal) =>
|
||||
proposal?.id && (
|
||||
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||
)
|
||||
)}
|
||||
{compact(proposals).map((proposal) => {
|
||||
return <ProposalsListItem key={proposal.id} proposal={proposal} />;
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="mt-6">
|
||||
@@ -175,7 +173,6 @@ export const ValidatorDetailsLink = ({
|
||||
};
|
||||
|
||||
const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -186,11 +183,6 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
pollInterval: 5000,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -212,15 +204,18 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
|
||||
useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
const proposals = useMemo(
|
||||
() =>
|
||||
proposalsData
|
||||
? getNotRejectedProposals(
|
||||
removePaginationWrapper(proposalsData.proposalsConnection?.edges)
|
||||
const proposals = useMemo(() => {
|
||||
if (!proposalsData?.proposalsConnection?.edges?.length) return [];
|
||||
return proposalsData
|
||||
? getNotRejectedProposals(
|
||||
compact(
|
||||
proposalsData.proposalsConnection.edges.map(
|
||||
(edge) => edge?.proposalNode
|
||||
)
|
||||
)
|
||||
: [],
|
||||
[proposalsData]
|
||||
);
|
||||
)
|
||||
: [];
|
||||
}, [proposalsData]);
|
||||
|
||||
const sortedProposals = useMemo(
|
||||
() => orderByDate(proposals).reverse(),
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
fragment UpdateMarketStates on UpdateMarketState {
|
||||
__typename
|
||||
updateType
|
||||
market {
|
||||
decimalPlaces
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
... on Perpetual {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
updateType
|
||||
price
|
||||
}
|
||||
|
||||
fragment UpdateReferralPrograms on UpdateReferralProgram {
|
||||
__typename
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateVolumeDiscountPrograms on UpdateVolumeDiscountProgram {
|
||||
__typename
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment IUpdateMarketFields on UpdateMarket {
|
||||
__typename
|
||||
marketId
|
||||
updateMarketConfiguration {
|
||||
instrument {
|
||||
code
|
||||
product {
|
||||
... on UpdateFutureProduct {
|
||||
quoteName
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# dataSourceSpecForTradingTermination {
|
||||
# sourceType {
|
||||
# ... on DataSourceDefinitionInternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfigurationTime {
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ... on DataSourceDefinitionExternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfiguration {
|
||||
# signers {
|
||||
# signer {
|
||||
# ... on PubKey {
|
||||
# key
|
||||
# }
|
||||
# ... on ETHAddress {
|
||||
# address
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# filters {
|
||||
# key {
|
||||
# name
|
||||
# type
|
||||
# }
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
}
|
||||
... on UpdatePerpetualProduct {
|
||||
quoteName
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
settlementScheduleProperty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
riskParameters {
|
||||
... on UpdateMarketSimpleRiskModel {
|
||||
simple {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
... on UpdateMarketLogNormalRiskModel {
|
||||
logNormal {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
r
|
||||
sigma
|
||||
mu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment INewMarketFields on NewMarket {
|
||||
__typename
|
||||
decimalPlaces
|
||||
metadata
|
||||
riskParameters {
|
||||
... on LogNormalRiskModel {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
mu
|
||||
r
|
||||
sigma
|
||||
}
|
||||
}
|
||||
... on SimpleRiskModel {
|
||||
params {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
}
|
||||
successorConfiguration {
|
||||
parentMarketId
|
||||
}
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
positionDecimalPlaces
|
||||
linearSlippageFactor
|
||||
}
|
||||
|
||||
# I prefix due to clash in lib/proposals
|
||||
fragment INewAssetFields on NewAsset {
|
||||
__typename
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
withdrawThreshold
|
||||
lifetimeLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment IUpdateAssetFields on UpdateAsset {
|
||||
__typename
|
||||
assetId
|
||||
quantum
|
||||
source {
|
||||
... on UpdateERC20 {
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment IUpdateNetworkParameterFields on UpdateNetworkParameter {
|
||||
__typename
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fragment VoteFields on ProposalVotes {
|
||||
yes {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
no {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
}
|
||||
|
||||
fragment ProposalTermsFields on ProposalTerms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
__typename
|
||||
...UpdateMarketStates
|
||||
...UpdateReferralPrograms
|
||||
...UpdateVolumeDiscountPrograms
|
||||
...INewMarketFields
|
||||
...IUpdateMarketFields
|
||||
...INewAssetFields
|
||||
...IUpdateNetworkParameterFields
|
||||
...IUpdateAssetFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment ProposalFields on Proposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
errorDetails
|
||||
terms {
|
||||
...ProposalTermsFields
|
||||
}
|
||||
votes {
|
||||
...VoteFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment BatchProposalFields on BatchProposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
errorDetails
|
||||
batchTerms {
|
||||
closingDatetime
|
||||
changes {
|
||||
enactmentDatetime
|
||||
}
|
||||
}
|
||||
subProposals {
|
||||
datetime
|
||||
terms {
|
||||
...ProposalTermsFields
|
||||
}
|
||||
}
|
||||
votes {
|
||||
...VoteFields
|
||||
}
|
||||
}
|
||||
|
||||
query Proposals {
|
||||
proposalsConnection {
|
||||
edges {
|
||||
proposalNode {
|
||||
__typename
|
||||
... on Proposal {
|
||||
...ProposalFields
|
||||
}
|
||||
... on BatchProposal {
|
||||
...BatchProposalFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query Proposal($proposalId: ID!) {
|
||||
proposal(id: $proposalId) {
|
||||
... on Proposal {
|
||||
...ProposalFields
|
||||
}
|
||||
... on BatchProposal {
|
||||
...BatchProposalFields
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+6
-2
@@ -3,9 +3,13 @@ import { ProposalState } from '@vegaprotocol/types';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import { type ReactNode } from 'react';
|
||||
import { type ProposalInfoLabelVariant } from '../proposal-info-label';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
|
||||
export const CurrentProposalState = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
let proposalStatus: ReactNode;
|
||||
let variant = 'tertiary' as ProposalInfoLabelVariant;
|
||||
|
||||
@@ -79,13 +79,22 @@ export const ListAsset = ({
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (data.asset.source.__typename !== 'ERC20') return null;
|
||||
|
||||
if (data.asset.source.__typename !== 'ERC20') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.asset.status !== Schema.AssetStatus.STATUS_PENDING_LISTING) {
|
||||
return null;
|
||||
}
|
||||
if (errorAsset || errorBundle) return null;
|
||||
|
||||
if (errorAsset || errorBundle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { assetSource, signatures, vegaAssetId, nonce } =
|
||||
assetData.erc20ListAssetBundle;
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<h3 className="mb-2 text-xl">{t('ListAsset')}</h3>
|
||||
|
||||
+40
-6
@@ -2,19 +2,53 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import { AssetDetail, AssetDetailsTable } from '@vegaprotocol/assets';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import {
|
||||
AssetDetail,
|
||||
AssetDetailsTable,
|
||||
useAssetQuery,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import {
|
||||
type INewAssetFieldsFragment,
|
||||
type IUpdateAssetFieldsFragment,
|
||||
} from '../../__generated__/Proposals';
|
||||
|
||||
export const ProposalAssetDetails = ({
|
||||
asset,
|
||||
originalAsset,
|
||||
change,
|
||||
assetId,
|
||||
}: {
|
||||
asset: AssetFieldsFragment;
|
||||
originalAsset?: AssetFieldsFragment;
|
||||
change: IUpdateAssetFieldsFragment | INewAssetFieldsFragment;
|
||||
assetId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showAssetDetails, setShowAssetDetails] = useState(false);
|
||||
|
||||
const { data } = useAssetQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
variables: {
|
||||
assetId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
let asset = removePaginationWrapper(data?.assetsConnection?.edges)[0];
|
||||
|
||||
const originalAsset = asset;
|
||||
|
||||
if (change.__typename === 'UpdateAsset') {
|
||||
asset = {
|
||||
...asset,
|
||||
quantum: change.quantum,
|
||||
source: { ...asset.source },
|
||||
};
|
||||
|
||||
if (asset.source.__typename === 'ERC20') {
|
||||
asset.source.lifetimeLimit = change.source.lifetimeLimit;
|
||||
asset.source.withdrawThreshold = change.source.withdrawThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section data-testid="proposal-asset-details">
|
||||
<CollapsibleToggle
|
||||
|
||||
+35
-19
@@ -6,16 +6,46 @@ import {
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
interface ProposalChangeTableProps {
|
||||
proposal: Proposal;
|
||||
proposal: Proposal | BatchProposal;
|
||||
}
|
||||
|
||||
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const terms = proposal?.terms;
|
||||
const closingTimeRow =
|
||||
proposal.__typename === 'Proposal' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.terms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{formatDateWithLocalTimezone(new Date(proposal.terms?.closingDatetime))}
|
||||
</KeyValueTableRow>
|
||||
) : proposal.__typename === 'BatchProposal' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.batchTerms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(proposal.batchTerms?.closingDatetime)
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
) : null;
|
||||
|
||||
const enactmentRow =
|
||||
proposal.__typename === 'Proposal' &&
|
||||
proposal.terms.change.__typename !== 'NewFreeform' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.terms?.enactmentDatetime || 0))
|
||||
? t('proposedEnactment')
|
||||
: t('enactedOn')}
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(proposal.terms?.enactmentDatetime || 0)
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
@@ -24,22 +54,8 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
{t('id')}
|
||||
{proposal?.id}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{formatDateWithLocalTimezone(new Date(terms?.closingDatetime))}
|
||||
</KeyValueTableRow>
|
||||
{terms?.change.__typename !== 'NewFreeform' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.enactmentDatetime || 0))
|
||||
? t('proposedEnactment')
|
||||
: t('enactedOn')}
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(terms?.enactmentDatetime || 0)
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
{closingTimeRow}
|
||||
{enactmentRow}
|
||||
<KeyValueTableRow>
|
||||
{t('proposedBy')}
|
||||
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
|
||||
|
||||
+54
-31
@@ -18,20 +18,21 @@ import {
|
||||
nextWeek,
|
||||
mockWalletContext,
|
||||
createUserVoteQueryMock,
|
||||
networkParamsQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { VoteState } from '../vote-details/use-user-vote';
|
||||
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
InstrumentDetailsDocument,
|
||||
useNewTransferProposalDetails,
|
||||
type InstrumentDetailsQuery,
|
||||
type InstrumentDetailsQueryVariables,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
jest.mock('@vegaprotocol/proposals', () => ({
|
||||
...jest.requireActual('@vegaprotocol/proposals'),
|
||||
useSuccessorMarketProposalDetails: () => ({
|
||||
code: 'PARENT_CODE',
|
||||
parentMarketId: 'PARENT_ID',
|
||||
}),
|
||||
useNewTransferProposalDetails: jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -44,7 +45,7 @@ const renderComponent = (
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<BrowserRouter>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MockedProvider mocks={[networkParamsQueryMock, ...mocks]}>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalHeader
|
||||
proposal={proposal}
|
||||
@@ -61,10 +62,39 @@ describe('Proposal header', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Renders New market proposal', () => {
|
||||
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
|
||||
|
||||
it('Renders New market proposal', async () => {
|
||||
const parentMarketId = 'parent-id';
|
||||
const parentCode = 'parent-code';
|
||||
const parentName = 'parent-name';
|
||||
const mock: MockedResponse<
|
||||
InstrumentDetailsQuery,
|
||||
InstrumentDetailsQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: InstrumentDetailsDocument,
|
||||
variables: {
|
||||
marketId: parentMarketId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
code: parentCode,
|
||||
name: parentName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New some market',
|
||||
@@ -73,6 +103,9 @@ describe('Proposal header', () => {
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
successorConfiguration: {
|
||||
parentMarketId,
|
||||
},
|
||||
instrument: {
|
||||
__typename: 'InstrumentConfiguration',
|
||||
name: 'Some market',
|
||||
@@ -87,7 +120,9 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
undefined,
|
||||
[mock]
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New some market'
|
||||
@@ -96,14 +131,13 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'tGBP settled future.'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
|
||||
'PARENT_CODE'
|
||||
);
|
||||
expect(
|
||||
await screen.findByTestId('proposal-successor-info')
|
||||
).toHaveTextContent(parentCode);
|
||||
});
|
||||
|
||||
it('Renders Update market proposal', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New market id',
|
||||
@@ -126,13 +160,12 @@ describe('Proposal header', () => {
|
||||
screen.queryByTestId('proposal-description')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Update to market ID: MarketId'
|
||||
'Update to market: MarketId'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders New asset proposal - ERC20', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New asset: Fake currency',
|
||||
@@ -162,8 +195,10 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders New asset proposal - BuiltInAsset', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New asset',
|
||||
},
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
@@ -177,9 +212,7 @@ describe('Proposal header', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New asset proposal'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('New asset');
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Symbol: BIA. Max faucet amount mint: 300'
|
||||
@@ -188,7 +221,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Update network', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'Network parameter',
|
||||
@@ -218,7 +250,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Freeform proposal - short rationale', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
id: 'short',
|
||||
rationale: {
|
||||
@@ -240,7 +271,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
id: 'long',
|
||||
rationale: {
|
||||
@@ -266,7 +296,6 @@ describe('Proposal header', () => {
|
||||
// Remove once proposals have rationale and re-enable above tests
|
||||
it('Renders Freeform proposal - id for title', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
id: 'freeform id',
|
||||
rationale: {
|
||||
@@ -323,7 +352,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Enacted', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
@@ -336,7 +364,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Passed', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
@@ -350,7 +377,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Waiting for node vote', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
terms: {
|
||||
@@ -365,7 +391,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Open', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
@@ -434,8 +459,6 @@ describe('Proposal header', () => {
|
||||
});
|
||||
});
|
||||
|
||||
jest.mock('@vegaprotocol/proposals');
|
||||
|
||||
describe('<NewTransferSummary />', () => {
|
||||
it('renders null if no details are provided', () => {
|
||||
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
|
||||
|
||||
+537
-220
@@ -1,4 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
Lozenge,
|
||||
@@ -8,14 +8,13 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { shorten } from '@vegaprotocol/utils';
|
||||
import { Heading, SubHeading } from '../../../../components/heading';
|
||||
import { type ReactNode } from 'react';
|
||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import {
|
||||
useCancelTransferProposalDetails,
|
||||
useInstrumentDetailsQuery,
|
||||
useNewTransferProposalDetails,
|
||||
useSuccessorMarketProposalDetails,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import {
|
||||
CONSOLE_MARKET_PAGE,
|
||||
@@ -27,217 +26,542 @@ import Routes from '../../../routes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { type VoteState } from '../vote-details/use-user-vote';
|
||||
import { VoteBreakdown } from '../vote-breakdown';
|
||||
import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
|
||||
import { type Proposal } from '../../types';
|
||||
import {
|
||||
GovernanceTransferKindMapping,
|
||||
type ProposalRejectionReason,
|
||||
ProposalRejectionReasonMapping,
|
||||
ProposalState,
|
||||
} from '@vegaprotocol/types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
|
||||
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import { MarketName } from '../proposal/market-name';
|
||||
import { Indicator } from '../proposal/indicator';
|
||||
|
||||
const ProposalTypeTags = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
return (
|
||||
<div data-testid="proposal-type">
|
||||
<ProposalTypeTag terms={proposal.terms} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal') {
|
||||
return (
|
||||
<div data-testid="proposal-type">
|
||||
<ProposalInfoLabel variant="secondary">BatchProposal</ProposalInfoLabel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ProposalTypeTag = ({ terms }: { terms: ProposalTermsFieldsFragment }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
switch (terms.change.__typename) {
|
||||
// Speical case for markets where we want to show the product type in the tag
|
||||
case 'NewMarket': {
|
||||
return (
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(
|
||||
terms.change?.instrument?.product?.__typename
|
||||
? `NewMarket${terms.change.instrument.product.__typename}`
|
||||
: 'NewMarket'
|
||||
)}
|
||||
</ProposalInfoLabel>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(terms.change.__typename)}
|
||||
</ProposalInfoLabel>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ProposalDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const featureFlags = useFeatureFlags((store) => store.flags);
|
||||
const consoleLink = useLinks(DApp.Console);
|
||||
|
||||
const renderDetails = (terms: ProposalTermsFieldsFragment) => {
|
||||
switch (terms.change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
const getAsset = (terms: ProposalTermsFieldsFragment) => {
|
||||
if (
|
||||
terms?.change.__typename === 'NewMarket' &&
|
||||
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
|
||||
terms.change.instrument.product?.__typename ===
|
||||
'PerpetualProduct')
|
||||
) {
|
||||
return terms.change.instrument.product.settlementAsset;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{terms.change.successorConfiguration && (
|
||||
<ParentMarketCode
|
||||
parentMarketId={
|
||||
terms.change.successorConfiguration.parentMarketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{t('Code')}: {terms.change.instrument.code}.
|
||||
</span>{' '}
|
||||
{terms && getAsset(terms)?.symbol ? (
|
||||
<>
|
||||
<span className="font-semibold">{getAsset(terms)?.symbol}</span>{' '}
|
||||
{t('settled future')}.
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
const marketPageLink = consoleLink(
|
||||
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.market.id)
|
||||
);
|
||||
return (
|
||||
<span>
|
||||
{featureFlags.UPDATE_MARKET_STATE &&
|
||||
terms.change?.market?.id &&
|
||||
terms.change.updateType ? (
|
||||
<>
|
||||
<span>{t(terms.change.updateType)}: </span>
|
||||
<span className="inline-flex gap-2">
|
||||
<span className="break-all">
|
||||
<MarketName marketId={terms.change.market.id} />
|
||||
</span>
|
||||
<span className="inline-flex items-end gap-0">
|
||||
<CopyWithTooltip
|
||||
text={terms.change.market.id}
|
||||
description={t('copyId')}
|
||||
>
|
||||
<button className="inline-block px-1">
|
||||
<VegaIcon size={20} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
<Tooltip description={t('OpenInConsole')} align="center">
|
||||
<Link
|
||||
className="inline-block px-1"
|
||||
to={marketPageLink}
|
||||
target="_blank"
|
||||
>
|
||||
<VegaIcon
|
||||
size={20}
|
||||
name={VegaIconNames.OPEN_EXTERNAL}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
const marketPageLink = consoleLink(
|
||||
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.marketId)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{t('UpdateToMarket')}: </span>
|
||||
<span className="inline-flex items-start gap-2">
|
||||
<span className="break-all">
|
||||
<MarketName marketId={terms.change.marketId} />
|
||||
</span>
|
||||
<span className="inline-flex items-end gap-0">
|
||||
<CopyWithTooltip
|
||||
text={terms.change.marketId}
|
||||
description={t('copyId')}
|
||||
>
|
||||
<button className="inline-block px-1">
|
||||
<VegaIcon size={20} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
<Tooltip description={t('OpenInConsole')} align="center">
|
||||
<Link
|
||||
className="inline-block px-1"
|
||||
target="_blank"
|
||||
to={marketPageLink}
|
||||
>
|
||||
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'UpdateReferralProgram': {
|
||||
return null;
|
||||
}
|
||||
case 'UpdateVolumeDiscountProgram': {
|
||||
return null;
|
||||
}
|
||||
case 'NewAsset': {
|
||||
return (
|
||||
<>
|
||||
<span>{t('Symbol')}:</span>{' '}
|
||||
<Lozenge>{terms.change.symbol}.</Lozenge>{' '}
|
||||
{terms.change.source.__typename === 'ERC20' && (
|
||||
<>
|
||||
<span>{t('ERC20ContractAddress')}:</span>{' '}
|
||||
<Lozenge>{terms.change.source.contractAddress}</Lozenge>
|
||||
</>
|
||||
)}{' '}
|
||||
{terms.change.source.__typename === 'BuiltinAsset' && (
|
||||
<>
|
||||
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
|
||||
<Lozenge>{terms.change.source.maxFaucetAmountMint}</Lozenge>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
return (
|
||||
<Trans
|
||||
i18nKey="Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>"
|
||||
values={{
|
||||
key: terms.change.networkParameter.key,
|
||||
value: terms.change.networkParameter.value,
|
||||
}}
|
||||
components={{
|
||||
// @ts-ignore children passed by i18next
|
||||
lozenge: <Lozenge className="text-xs" />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'NewFreeform': {
|
||||
return <span />;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
return (
|
||||
<Trans
|
||||
i18nKey="Asset ID: <lozenge>{{id}}</lozenge>"
|
||||
values={{
|
||||
id: truncateMiddle(terms.change.assetId),
|
||||
}}
|
||||
components={{
|
||||
// @ts-ignore children passed by i18next
|
||||
lozenge: <Lozenge />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'NewTransfer':
|
||||
return featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<NewTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
case 'CancelTransfer':
|
||||
return featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<CancelTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let details = null;
|
||||
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
details = (
|
||||
<div>
|
||||
<div>{renderDetails(proposal.terms)}</div>
|
||||
<VoteStateText
|
||||
state={proposal.state}
|
||||
closingDatetime={proposal.terms.closingDatetime}
|
||||
enactmentDatetime={proposal.terms.enactmentDatetime}
|
||||
rejectionReason={proposal.rejectionReason}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal' && proposal.subProposals) {
|
||||
details = (
|
||||
<div>
|
||||
<h3 className="text-xl border-b border-default pb-3 mb-3">
|
||||
Proposals in batch
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-2 border-b border-default pb-3 mb-3">
|
||||
{proposal.subProposals.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 items-center"
|
||||
>
|
||||
<Indicator indicator={i + 1} />
|
||||
<span>
|
||||
<div>{renderDetails(p.terms)}</div>
|
||||
<SubProposalStateText
|
||||
state={proposal.state}
|
||||
enactmentDatetime={p.terms.enactmentDatetime}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<BatchProposalStateText
|
||||
state={proposal.state}
|
||||
closingDatetime={proposal.batchTerms?.closingDatetime}
|
||||
rejectionReason={proposal.rejectionReason}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="proposal-details"
|
||||
className="break-words mb-6 text-vega-light-200"
|
||||
>
|
||||
{details}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VoteStateText = ({
|
||||
state,
|
||||
closingDatetime,
|
||||
enactmentDatetime,
|
||||
rejectionReason,
|
||||
}: {
|
||||
state: ProposalState;
|
||||
closingDatetime: string;
|
||||
enactmentDatetime: string;
|
||||
rejectionReason: ProposalRejectionReason | null | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const nowToCloseInHours = differenceInHours(
|
||||
new Date(closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
|
||||
const props = {
|
||||
'data-testid': 'vote-details',
|
||||
};
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_PASSED:
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
return (
|
||||
<p {...props}>
|
||||
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
|
||||
{t('{{time}} left to vote', {
|
||||
time: formatDistanceToNowStrict(new Date(closingDatetime)),
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
return <p {...props}>{t(state)}</p>;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
const props = { 'data-testid': 'vote-status' };
|
||||
|
||||
if (rejectionReason) {
|
||||
return (
|
||||
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <p {...props}>{t('Proposal rejected')}</p>;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders state details relevant to the sub proposal, namely the enactment
|
||||
* date and time
|
||||
*/
|
||||
const SubProposalStateText = ({
|
||||
state,
|
||||
enactmentDatetime,
|
||||
}: {
|
||||
state: ProposalState;
|
||||
enactmentDatetime: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const props = {
|
||||
'data-testid': 'vote-details',
|
||||
className: 'm-0',
|
||||
};
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_OPEN:
|
||||
case ProposalState.STATE_PASSED:
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_REJECTED:
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
// If voting is still open we render a single clost time for all sub proposals
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders state details relevant for the entire batch. IE. if the proposal was
|
||||
* rejected or declined, or the vote close time. Does not render enactment times as
|
||||
* those are relevant to the sub proposal
|
||||
*/
|
||||
const BatchProposalStateText = ({
|
||||
state,
|
||||
closingDatetime,
|
||||
rejectionReason,
|
||||
}: {
|
||||
state: ProposalState;
|
||||
closingDatetime: string;
|
||||
rejectionReason: ProposalRejectionReason | null | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const nowToCloseInHours = differenceInHours(
|
||||
new Date(closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
|
||||
const props = {
|
||||
'data-testid': 'vote-details',
|
||||
};
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED:
|
||||
case ProposalState.STATE_PASSED:
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
return null;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
return (
|
||||
<p {...props}>
|
||||
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
|
||||
{t('{{time}} left to vote', {
|
||||
time: formatDistanceToNowStrict(new Date(closingDatetime)),
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
return <p {...props}>{t(state)}</p>;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
const props = { 'data-testid': 'vote-status' };
|
||||
|
||||
if (rejectionReason) {
|
||||
return (
|
||||
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <p {...props}>{t('Proposal rejected')}</p>;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ProposalHeader = ({
|
||||
proposal,
|
||||
isListItem = true,
|
||||
voteState,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
proposal: Proposal | BatchProposal;
|
||||
isListItem?: boolean;
|
||||
voteState?: VoteState | null;
|
||||
}) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const change = proposal?.terms.change;
|
||||
|
||||
const consoleLink = useLinks(DApp.Console);
|
||||
|
||||
let details: ReactNode;
|
||||
let proposalType = '';
|
||||
let fallbackTitle = '';
|
||||
|
||||
const title = proposal?.rationale.title.trim();
|
||||
|
||||
const fallbackTitle = t(
|
||||
proposal.__typename === 'Proposal'
|
||||
? 'Unknown proposal'
|
||||
: 'Unknown batch proposal'
|
||||
);
|
||||
const titleContent = shorten(title ?? '', 100);
|
||||
|
||||
const getAsset = (proposal: Proposal) => {
|
||||
const terms = proposal?.terms;
|
||||
if (
|
||||
terms?.change.__typename === 'NewMarket' &&
|
||||
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
|
||||
terms.change.instrument.product?.__typename === 'PerpetualProduct')
|
||||
) {
|
||||
return terms.change.instrument.product.settlementAsset;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
switch (change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
proposalType =
|
||||
featureFlags.PRODUCT_PERPETUALS &&
|
||||
change?.instrument?.product?.__typename
|
||||
? `NewMarket${change?.instrument?.product?.__typename}`
|
||||
: 'NewMarket';
|
||||
fallbackTitle = t('NewMarketProposal');
|
||||
details = (
|
||||
<>
|
||||
{featureFlags.SUCCESSOR_MARKETS && (
|
||||
<SuccessorCode proposalId={proposal?.id} />
|
||||
)}
|
||||
<span>
|
||||
{t('Code')}: {change.instrument.code}.
|
||||
</span>{' '}
|
||||
{proposal?.terms && getAsset(proposal)?.symbol ? (
|
||||
<>
|
||||
<span className="font-semibold">
|
||||
{getAsset(proposal)?.symbol}
|
||||
</span>{' '}
|
||||
{t('settled future')}.
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
proposalType =
|
||||
featureFlags.UPDATE_MARKET_STATE && change?.updateType
|
||||
? t(change.updateType)
|
||||
: 'UpdateMarketState';
|
||||
fallbackTitle = t('UpdateMarketStateProposal');
|
||||
details = (
|
||||
<span>
|
||||
{featureFlags.UPDATE_MARKET_STATE &&
|
||||
change?.market?.id &&
|
||||
change.updateType ? (
|
||||
<>
|
||||
{t(change.updateType)}: {truncateMiddle(change.market.id)}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
proposalType = 'UpdateMarket';
|
||||
fallbackTitle = t('UpdateMarketProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('UpdateToMarket')}:</span>{' '}
|
||||
<span className="inline-flex items-start gap-2">
|
||||
<span className="break-all">{change.marketId} </span>
|
||||
<span className="inline-flex items-end gap-0">
|
||||
<CopyWithTooltip
|
||||
text={change.marketId}
|
||||
description={t('copyToClipboard')}
|
||||
>
|
||||
<button className="inline-block px-1">
|
||||
<VegaIcon size={20} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
<Tooltip description={t('OpenInConsole')} align="center">
|
||||
<button
|
||||
className="inline-block px-1"
|
||||
onClick={() => {
|
||||
const marketPageLink = consoleLink(
|
||||
CONSOLE_MARKET_PAGE.replace(':marketId', change.marketId)
|
||||
);
|
||||
window.open(marketPageLink, '_blank');
|
||||
}}
|
||||
>
|
||||
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateReferralProgram': {
|
||||
proposalType = 'UpdateReferralProgram';
|
||||
fallbackTitle = t('UpdateReferralProgramProposal');
|
||||
break;
|
||||
}
|
||||
case 'UpdateVolumeDiscountProgram': {
|
||||
proposalType = 'UpdateVolumeDiscountProgram';
|
||||
fallbackTitle = t('UpdateVolumeDiscountProgramProposal');
|
||||
break;
|
||||
}
|
||||
case 'NewAsset': {
|
||||
proposalType = 'NewAsset';
|
||||
fallbackTitle = t('NewAssetProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('Symbol')}:</span> <Lozenge>{change.symbol}.</Lozenge>{' '}
|
||||
{change.source.__typename === 'ERC20' && (
|
||||
<>
|
||||
<span>{t('ERC20ContractAddress')}:</span>{' '}
|
||||
<Lozenge>{change.source.contractAddress}</Lozenge>
|
||||
</>
|
||||
)}{' '}
|
||||
{change.source.__typename === 'BuiltinAsset' && (
|
||||
<>
|
||||
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
|
||||
<Lozenge>{change.source.maxFaucetAmountMint}</Lozenge>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
proposalType = 'NetworkParameter';
|
||||
fallbackTitle = t('NetworkParameterProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('Change')}:</span>{' '}
|
||||
<Lozenge>{change.networkParameter.key}</Lozenge>{' '}
|
||||
<span>{t('to')}</span>{' '}
|
||||
<span className="whitespace-nowrap">
|
||||
<Lozenge>{change.networkParameter.value}</Lozenge>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewFreeform': {
|
||||
proposalType = 'Freeform';
|
||||
fallbackTitle = t('FreeformProposal');
|
||||
details = <span />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
proposalType = 'UpdateAsset';
|
||||
fallbackTitle = t('UpdateAssetProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('AssetID')}:</span>{' '}
|
||||
<Lozenge>{truncateMiddle(change.assetId)}</Lozenge>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewTransfer':
|
||||
proposalType = 'NewTransfer';
|
||||
fallbackTitle = t('NewTransferProposal');
|
||||
details = featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<NewTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
break;
|
||||
case 'CancelTransfer':
|
||||
proposalType = 'CancelTransfer';
|
||||
fallbackTitle = t('CancelTransferProposal');
|
||||
details = featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<CancelTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-4 mb-6 text-sm">
|
||||
<div data-testid="proposal-type">
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(`${proposalType}`)}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
<ProposalTypeTags proposal={proposal} />
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{(voteState === 'Yes' || voteState === 'No') && (
|
||||
@@ -264,50 +588,43 @@ export const ProposalHeader = ({
|
||||
<div data-testid="proposal-title" className="break-all">
|
||||
{isListItem ? (
|
||||
<header>
|
||||
<SubHeading
|
||||
title={titleContent || fallbackTitle || t('Unknown proposal')}
|
||||
/>
|
||||
<SubHeading title={titleContent || fallbackTitle} />
|
||||
</header>
|
||||
) : (
|
||||
<Heading
|
||||
title={titleContent || fallbackTitle || t('Unknown proposal')}
|
||||
/>
|
||||
<Heading title={titleContent || fallbackTitle} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{details && (
|
||||
<div
|
||||
data-testid="proposal-details"
|
||||
className="break-words mb-6 text-vega-light-200"
|
||||
>
|
||||
{details}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProposalDetails proposal={proposal} />
|
||||
<VoteBreakdown proposal={proposal} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SuccessorCode = ({
|
||||
proposalId,
|
||||
export const ParentMarketCode = ({
|
||||
parentMarketId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
parentMarketId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const successor = useSuccessorMarketProposalDetails(proposalId);
|
||||
const { data } = useInstrumentDetailsQuery({
|
||||
variables: {
|
||||
marketId: parentMarketId,
|
||||
},
|
||||
});
|
||||
|
||||
return successor.parentMarketId || successor.code ? (
|
||||
if (!data?.market?.tradableInstrument.instrument.code) return null;
|
||||
|
||||
return (
|
||||
<span className="block" data-testid="proposal-successor-info">
|
||||
{t('Successor market to')}:{' '}
|
||||
<Link
|
||||
to={`${Routes.PROPOSALS}/${successor.parentMarketId}`}
|
||||
to={`${Routes.PROPOSALS}/${parentMarketId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{successor.code || successor.parentMarketId}
|
||||
{data.market.tradableInstrument.instrument.code}
|
||||
</Link>
|
||||
</span>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
export const NewTransferSummary = ({
|
||||
|
||||
@@ -3,14 +3,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
export const ProposalJson = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}) => {
|
||||
export const ProposalJson = ({ proposal }: { proposal?: unknown }) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
@@ -24,7 +18,7 @@ export const ProposalJson = ({
|
||||
<SubHeading title={t('proposalJson')} />
|
||||
</CollapsibleToggle>
|
||||
|
||||
{showDetails && <SyntaxHighlighter data={proposal} />}
|
||||
{showDetails && <SyntaxHighlighter size="smaller" data={proposal} />}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-15
@@ -5,6 +5,11 @@ import {
|
||||
} from './proposal-market-changes';
|
||||
import type { JsonValue } from '../../../../components/json-diff';
|
||||
|
||||
jest.mock('../proposal/market-name.tsx', () => ({
|
||||
...jest.requireActual('../proposal/market-name.tsx'),
|
||||
MarketName: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('applyImmutableKeysFromEarlierVersion', () => {
|
||||
it('returns an empty object if any argument is not an object or null', () => {
|
||||
const earlierVersion: JsonValue = null;
|
||||
@@ -57,33 +62,21 @@ describe('applyImmutableKeysFromEarlierVersion', () => {
|
||||
describe('ProposalMarketChanges', () => {
|
||||
it('renders correctly', () => {
|
||||
const { getByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
|
||||
);
|
||||
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('JsonDiff is not visible when showChanges is false', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
|
||||
);
|
||||
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('JsonDiff is visible when showChanges is true', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
|
||||
);
|
||||
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
|
||||
expect(getByTestId('json-diff')).toBeInTheDocument();
|
||||
|
||||
+100
-27
@@ -1,12 +1,24 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
import { JsonDiff } from '../../../../components/json-diff';
|
||||
import compact from 'lodash/compact';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { JsonDiff, type JsonValue } from '../../../../components/json-diff';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { JsonValue } from '../../../../components/json-diff';
|
||||
import {
|
||||
useFetchProposal,
|
||||
useFetchProposals,
|
||||
flatten,
|
||||
isBatchProposalNode,
|
||||
isSingleProposalNode,
|
||||
type ProposalNode,
|
||||
type SingleProposalData,
|
||||
type SubProposalData,
|
||||
} from '../proposal/proposal-utils';
|
||||
import { MarketName } from '../proposal/market-name';
|
||||
|
||||
const immutableKeys = [
|
||||
'decimalPlaces',
|
||||
@@ -16,8 +28,8 @@ const immutableKeys = [
|
||||
];
|
||||
|
||||
export const applyImmutableKeysFromEarlierVersion = (
|
||||
earlierVersion: JsonValue,
|
||||
updatedVersion: JsonValue
|
||||
earlierVersion: unknown,
|
||||
updatedVersion: unknown
|
||||
) => {
|
||||
if (
|
||||
typeof earlierVersion !== 'object' ||
|
||||
@@ -33,26 +45,94 @@ export const applyImmutableKeysFromEarlierVersion = (
|
||||
|
||||
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values
|
||||
immutableKeys.forEach((key) => {
|
||||
set(updatedVersionCopy, key, get(earlierVersion, key));
|
||||
const earlier = get(earlierVersion, key);
|
||||
if (earlier) set(updatedVersionCopy, key, earlier);
|
||||
});
|
||||
|
||||
return updatedVersionCopy;
|
||||
};
|
||||
|
||||
interface ProposalMarketChangesProps {
|
||||
originalProposal: JsonValue;
|
||||
latestEnactedProposal: JsonValue | undefined;
|
||||
updatedProposal: JsonValue;
|
||||
marketId: string;
|
||||
/** This are the changes from proposal */
|
||||
updateProposalNode: ProposalNode | null;
|
||||
indicator?: number;
|
||||
}
|
||||
|
||||
export const ProposalMarketChanges = ({
|
||||
originalProposal,
|
||||
latestEnactedProposal,
|
||||
updatedProposal,
|
||||
marketId,
|
||||
updateProposalNode,
|
||||
indicator,
|
||||
}: ProposalMarketChangesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showChanges, setShowChanges] = useState(false);
|
||||
|
||||
const { data: originalProposalData } = useFetchProposal({
|
||||
proposalId: marketId,
|
||||
});
|
||||
|
||||
const { data: enactedProposalsData } = useFetchProposals({
|
||||
proposalState: 'STATE_ENACTED',
|
||||
proposalType: 'TYPE_UPDATE_MARKET',
|
||||
});
|
||||
|
||||
let updateProposal: SingleProposalData | SubProposalData | undefined;
|
||||
if (isBatchProposalNode(updateProposalNode)) {
|
||||
updateProposal = updateProposalNode.proposals.find(
|
||||
(p, i) =>
|
||||
p.terms.updateMarket?.marketId === marketId &&
|
||||
(indicator != null ? i === indicator - 1 : true)
|
||||
);
|
||||
}
|
||||
if (isSingleProposalNode(updateProposalNode)) {
|
||||
updateProposal = updateProposalNode.proposal;
|
||||
}
|
||||
|
||||
// this should get the proposal before the current one
|
||||
const enactedUpdateMarketProposals = orderBy(
|
||||
compact(
|
||||
flatten(enactedProposalsData).filter((enacted) => {
|
||||
const related = enacted.terms.updateMarket?.marketId === marketId;
|
||||
const notCurrent =
|
||||
enacted.id !== updateProposal?.id ||
|
||||
('batchId' in enacted && enacted.batchId !== updateProposal.id);
|
||||
const beforeCurrent =
|
||||
Number(enacted.terms.enactmentTimestamp) <
|
||||
Number(updateProposal?.terms.enactmentTimestamp);
|
||||
return related && notCurrent && beforeCurrent;
|
||||
})
|
||||
),
|
||||
[(proposal) => Number(proposal.terms.enactmentTimestamp)],
|
||||
'desc'
|
||||
);
|
||||
|
||||
const latestEnactedProposal =
|
||||
enactedUpdateMarketProposals.length > 0
|
||||
? enactedUpdateMarketProposals[0]
|
||||
: undefined;
|
||||
|
||||
let originalProposal;
|
||||
if (isBatchProposalNode(originalProposalData)) {
|
||||
originalProposal = originalProposalData.proposals.find(
|
||||
(proposal) => proposal.id === marketId && proposal.terms.newMarket != null
|
||||
);
|
||||
}
|
||||
if (isSingleProposalNode(originalProposalData)) {
|
||||
originalProposal = originalProposalData.proposal;
|
||||
}
|
||||
|
||||
// LEFT SIDE: update market proposal enacted just before this one
|
||||
// or original new market proposal
|
||||
const left =
|
||||
latestEnactedProposal?.terms.updateMarket?.changes ||
|
||||
originalProposal?.terms.newMarket?.changes;
|
||||
|
||||
// RIGHT SIDE: this update market proposal
|
||||
const right = applyImmutableKeysFromEarlierVersion(
|
||||
left,
|
||||
updateProposal?.terms.updateMarket?.changes
|
||||
);
|
||||
|
||||
return (
|
||||
<section data-testid="proposal-market-changes">
|
||||
<CollapsibleToggle
|
||||
@@ -60,25 +140,18 @@ export const ProposalMarketChanges = ({
|
||||
setToggleState={setShowChanges}
|
||||
dataTestId={'proposal-market-changes-toggle'}
|
||||
>
|
||||
<SubHeading title={t('updatesToMarket')} />
|
||||
<SubHeading
|
||||
title={
|
||||
<>
|
||||
{t('UpdateToMarket')}: <MarketName marketId={marketId} truncate />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</CollapsibleToggle>
|
||||
|
||||
{showChanges && (
|
||||
<div className="mb-6">
|
||||
<JsonDiff
|
||||
left={latestEnactedProposal || originalProposal}
|
||||
right={
|
||||
latestEnactedProposal
|
||||
? applyImmutableKeysFromEarlierVersion(
|
||||
latestEnactedProposal,
|
||||
updatedProposal
|
||||
)
|
||||
: applyImmutableKeysFromEarlierVersion(
|
||||
originalProposal,
|
||||
updatedProposal
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="mb-6 bg-vega-cdark-900 p-2 rounded-lg">
|
||||
<JsonDiff left={left as JsonValue} right={right as JsonValue} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
+21
-9
@@ -18,6 +18,7 @@ import {
|
||||
getDataSourceSpecForTradingTermination,
|
||||
getSigners,
|
||||
MarginScalingFactorsPanel,
|
||||
marketInfoProvider,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
Button,
|
||||
@@ -28,8 +29,8 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import type { MarketInfo } from '@vegaprotocol/markets';
|
||||
import { create } from 'zustand';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
type MarketDataDialogState = {
|
||||
isOpen: boolean;
|
||||
@@ -48,18 +49,29 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
|
||||
const marketDataHeaderStyles =
|
||||
'font-alpha calt text-base border-b border-vega-dark-200 mt-2 py-2';
|
||||
|
||||
export const ProposalMarketData = ({
|
||||
marketData,
|
||||
parentMarketData,
|
||||
}: {
|
||||
marketData: MarketInfo;
|
||||
parentMarketData?: MarketInfo;
|
||||
}) => {
|
||||
export const ProposalMarketData = ({ proposalId }: { proposalId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, open, close } = useMarketDataDialogStore();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
if (!marketData) {
|
||||
const { data: marketData } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: proposalId,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: parentMarketData } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
skip: !marketData?.parentMarketID,
|
||||
variables: {
|
||||
marketId: marketData?.parentMarketID || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!marketData || !parentMarketData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+43
-66
@@ -7,7 +7,6 @@ import {
|
||||
formatReferralRewardMultiplier,
|
||||
ProposalReferralProgramDetails,
|
||||
} from './proposal-referral-program-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
@@ -59,87 +58,65 @@ describe('ProposalReferralProgramDetails helper functions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const mockReferralProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateReferralProgram',
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 6,
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
referralDiscountFactor: '0.001',
|
||||
referralRewardFactor: '0.001',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 24,
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
referralDiscountFactor: '0.005',
|
||||
referralRewardFactor: '0.005',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 48,
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
referralDiscountFactor: '0.01',
|
||||
referralRewardFactor: '0.01',
|
||||
},
|
||||
],
|
||||
endOfProgram: '2026-10-03T10:34:34Z',
|
||||
windowLength: 3,
|
||||
stakingTiers: [
|
||||
{
|
||||
minimumStakedTokens: '1',
|
||||
referralRewardMultiplier: '1',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '2',
|
||||
referralRewardMultiplier: '2',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '5',
|
||||
referralRewardMultiplier: '3',
|
||||
},
|
||||
],
|
||||
const mockChange = {
|
||||
__typename: 'UpdateReferralProgram' as const,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 6,
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
referralDiscountFactor: '0.001',
|
||||
referralRewardFactor: '0.001',
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
minimumEpochs: 24,
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
referralDiscountFactor: '0.005',
|
||||
referralRewardFactor: '0.005',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 48,
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
referralDiscountFactor: '0.01',
|
||||
referralRewardFactor: '0.01',
|
||||
},
|
||||
],
|
||||
endOfProgram: '2026-10-03T10:34:34Z',
|
||||
windowLength: 3,
|
||||
stakingTiers: [
|
||||
{
|
||||
minimumStakedTokens: '1',
|
||||
referralRewardMultiplier: '1',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '2',
|
||||
referralRewardMultiplier: '2',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '5',
|
||||
referralRewardMultiplier: '3',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('<ProposalReferralProgramDetails />', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalReferralProgramDetails proposal={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-referral-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateReferralProgram', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<ProposalReferralProgramDetails proposal={updateMarketProposal} />);
|
||||
render(<ProposalReferralProgramDetails change={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-referral-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalReferralProgramDetails proposal={incompleteProposal} />);
|
||||
const emptyChange = {};
|
||||
// @ts-ignore change deliberately empty
|
||||
render(<ProposalReferralProgramDetails change={emptyChange} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-referral-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should render relevant fields if present', () => {
|
||||
render(<ProposalReferralProgramDetails proposal={mockReferralProposal} />);
|
||||
render(<ProposalReferralProgramDetails change={mockChange} />);
|
||||
expect(
|
||||
screen.getByTestId('proposal-referral-program-window-length')
|
||||
).toBeInTheDocument();
|
||||
|
||||
+9
-10
@@ -13,10 +13,10 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type UpdateReferralProgramsFragment } from '../../__generated__/Proposals';
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: Proposal | null;
|
||||
change: UpdateReferralProgramsFragment | null;
|
||||
}
|
||||
|
||||
export const formatEndOfProgramTimestamp = (value: string) => {
|
||||
@@ -44,20 +44,19 @@ export const formatReferralRewardMultiplier = (value: string) => {
|
||||
};
|
||||
|
||||
export const ProposalReferralProgramDetails = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
const { t } = useTranslation();
|
||||
if (proposal?.terms?.change?.__typename !== 'UpdateReferralProgram') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers.slice();
|
||||
const stakingTiers = proposal?.terms?.change?.stakingTiers.slice();
|
||||
const windowLength = proposal?.terms?.change?.windowLength;
|
||||
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
|
||||
if (change?.__typename !== 'UpdateReferralProgram') return null;
|
||||
|
||||
const benefitTiers = change?.benefitTiers.slice();
|
||||
const stakingTiers = change?.stakingTiers.slice();
|
||||
const windowLength = change?.windowLength;
|
||||
const endOfProgramTimestamp = change?.endOfProgram;
|
||||
|
||||
if (
|
||||
!benefitTiers &&
|
||||
|
||||
+3
-4
@@ -6,15 +6,14 @@ import {
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalCancelTransferDetails = ({
|
||||
proposal,
|
||||
proposalId,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
proposalId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const details = useCancelTransferProposalDetails(proposal?.id);
|
||||
const details = useCancelTransferProposalDetails(proposalId);
|
||||
|
||||
if (!details) {
|
||||
return null;
|
||||
|
||||
+3
-4
@@ -19,17 +19,16 @@ import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatDateWithLocalTimezone,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalTransferDetails = ({
|
||||
proposal,
|
||||
proposalId,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
proposalId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const details = useNewTransferProposalDetails(proposal?.id);
|
||||
const details = useNewTransferProposalDetails(proposalId);
|
||||
if (!details) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+51
-94
@@ -1,6 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
@@ -10,109 +9,71 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockVestingBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_quantum_balance: '10000',
|
||||
reward_multiplier: '0.05',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '500000000000',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '10000000000000',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
const mockChange1 = {
|
||||
__typename: 'UpdateNetworkParameter' as const,
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_quantum_balance: '10000',
|
||||
reward_multiplier: '0.05',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '500000000000',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '10000000000000',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mockActivityStreakBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_activity_streak: '10000',
|
||||
vesting_multiplier: '5',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: '10000000000000',
|
||||
vesting_multiplier: '100',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
const mockChange2 = {
|
||||
__typename: 'UpdateNetworkParameter' as const,
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_activity_streak: '10000',
|
||||
vesting_multiplier: '5',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: '10000000000000',
|
||||
vesting_multiplier: '100',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('ProposalUpdateBenefitTiers', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalUpdateBenefitTiers proposal={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateNetworkParameter', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
render(<ProposalUpdateBenefitTiers change={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are relevant fields that are empty', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({}),
|
||||
},
|
||||
},
|
||||
const incompleteProposal = {
|
||||
__typename: 'UpdateNetworkParameter' as const,
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
render(<ProposalUpdateBenefitTiers change={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render a valid vesting benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
|
||||
);
|
||||
render(<ProposalUpdateBenefitTiers change={mockChange1} />);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
@@ -133,11 +94,7 @@ describe('ProposalUpdateBenefitTiers', () => {
|
||||
});
|
||||
|
||||
it('should render a valid activity streak benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers
|
||||
proposal={mockActivityStreakBenefitTierProposal}
|
||||
/>
|
||||
);
|
||||
render(<ProposalUpdateBenefitTiers change={mockChange2} />);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
|
||||
+6
-6
@@ -11,7 +11,7 @@ import {
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type IUpdateNetworkParameterFieldsFragment } from '../../__generated__/Proposals';
|
||||
|
||||
// These types are not generated as it's not known how dynamic these are
|
||||
type VestingBenefitTier = {
|
||||
@@ -43,7 +43,7 @@ export const formatVolumeDiscountFactor = (value: string) => {
|
||||
};
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: Proposal | null;
|
||||
change: IUpdateNetworkParameterFieldsFragment | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,17 +55,17 @@ interface ProposalReferralProgramDetailsProps {
|
||||
* It only renders known fields so that they can be formatted correctly.
|
||||
*/
|
||||
export const ProposalUpdateBenefitTiers = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (
|
||||
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
|
||||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
|
||||
change?.__typename !== 'UpdateNetworkParameter' ||
|
||||
change?.networkParameter.key.slice(-13) !== '.benefitTiers'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
|
||||
const benefitTiersString = change?.networkParameter.value;
|
||||
const benefitTiers = getBenefitTiers(benefitTiersString);
|
||||
|
||||
if (!benefitTiers) {
|
||||
|
||||
+53
-61
@@ -1,86 +1,78 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { ProposalUpdateMarketState } from './proposal-update-market-state';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { MarketUpdateType } from '@vegaprotocol/types';
|
||||
|
||||
jest.mock('../proposal/market-name.tsx', () => ({
|
||||
...jest.requireActual('../proposal/market-name.tsx'),
|
||||
MarketName: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<ProposalUpdateMarketState />', () => {
|
||||
const suspendProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'suspendProposal Name',
|
||||
code: 'suspendProposal Code',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
const suspendProposal = {
|
||||
__typename: 'UpdateMarketState' as const,
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'suspendProposal Name',
|
||||
code: 'suspendProposal Code',
|
||||
product: {
|
||||
__typename: 'Future' as const,
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
|
||||
},
|
||||
},
|
||||
});
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
|
||||
};
|
||||
|
||||
const resumeProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'resumeProposal Name',
|
||||
code: 'resumeProposal Code',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
const resumeProposal = {
|
||||
__typename: 'UpdateMarketState' as const,
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'resumeProposal Name',
|
||||
code: 'resumeProposal Code',
|
||||
product: {
|
||||
__typename: 'Future' as const,
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
|
||||
},
|
||||
},
|
||||
});
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
|
||||
};
|
||||
|
||||
const terminateProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'terminateProposal Name',
|
||||
code: 'terminateProposal Code',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
const terminateProposal = {
|
||||
__typename: 'UpdateMarketState' as const,
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'terminateProposal Name',
|
||||
code: 'terminateProposal Code',
|
||||
product: {
|
||||
__typename: 'Future' as const,
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '123',
|
||||
},
|
||||
},
|
||||
});
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '123',
|
||||
};
|
||||
|
||||
it('should render nothing if proposal is null', () => {
|
||||
render(<ProposalUpdateMarketState proposal={null} />);
|
||||
render(<ProposalUpdateMarketState change={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-market-state')).toBeNull();
|
||||
});
|
||||
|
||||
it('should toggle details when CollapsibleToggle is clicked', () => {
|
||||
render(<ProposalUpdateMarketState proposal={suspendProposal} />);
|
||||
render(<ProposalUpdateMarketState change={suspendProposal} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('proposal-update-market-state-table')
|
||||
@@ -94,7 +86,7 @@ describe('<ProposalUpdateMarketState />', () => {
|
||||
});
|
||||
|
||||
it('should display suspend market information when showDetails is true', () => {
|
||||
render(<ProposalUpdateMarketState proposal={suspendProposal} />);
|
||||
render(<ProposalUpdateMarketState change={suspendProposal} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
|
||||
|
||||
@@ -103,7 +95,7 @@ describe('<ProposalUpdateMarketState />', () => {
|
||||
});
|
||||
|
||||
it('should display resume market information when showDetails is true', () => {
|
||||
render(<ProposalUpdateMarketState proposal={resumeProposal} />);
|
||||
render(<ProposalUpdateMarketState change={resumeProposal} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
|
||||
|
||||
@@ -112,7 +104,7 @@ describe('<ProposalUpdateMarketState />', () => {
|
||||
});
|
||||
|
||||
it('should display terminate market information when showDetails is true', () => {
|
||||
render(<ProposalUpdateMarketState proposal={terminateProposal} />);
|
||||
render(<ProposalUpdateMarketState change={terminateProposal} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
|
||||
|
||||
|
||||
+29
-15
@@ -8,29 +8,31 @@ import { Row } from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals';
|
||||
import { MarketUpdateTypeMapping } from '@vegaprotocol/types';
|
||||
import { MarketName } from '../proposal/market-name';
|
||||
|
||||
interface ProposalUpdateMarketStateProps {
|
||||
proposal: Proposal | null;
|
||||
change: UpdateMarketStatesFragment | null;
|
||||
}
|
||||
|
||||
export const ProposalUpdateMarketState = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalUpdateMarketStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
let market;
|
||||
let isTerminate = false;
|
||||
|
||||
if (!proposal) {
|
||||
if (!change || change.__typename !== 'UpdateMarketState') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (proposal?.terms.change.__typename === 'UpdateMarketState') {
|
||||
market = proposal?.terms?.change?.market;
|
||||
isTerminate =
|
||||
proposal?.terms?.change?.updateType ===
|
||||
'MARKET_STATE_UPDATE_TYPE_TERMINATE';
|
||||
const market = change?.market;
|
||||
const isTerminate =
|
||||
change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
|
||||
|
||||
let toggleTitle = t(change.updateType);
|
||||
if (toggleTitle.length === 0) {
|
||||
toggleTitle = t('MarketDetails');
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -40,17 +42,29 @@ export const ProposalUpdateMarketState = ({
|
||||
setToggleState={setShowDetails}
|
||||
dataTestId="proposal-market-data-toggle"
|
||||
>
|
||||
<SubHeading title={t('MarketDetails')} />
|
||||
<SubHeading
|
||||
title={
|
||||
<>
|
||||
{toggleTitle}: <MarketName marketId={market?.id} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</CollapsibleToggle>
|
||||
|
||||
{showDetails && (
|
||||
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
|
||||
{proposal?.terms.change.__typename === 'UpdateMarketState' && (
|
||||
{change.__typename === 'UpdateMarketState' && (
|
||||
<KeyValueTable data-testid="proposal-update-market-state-table">
|
||||
<KeyValueTableRow>
|
||||
{t('marketId')}
|
||||
{market?.id}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('State')}
|
||||
<span className="bg-vega-green-650 px-1">
|
||||
{MarketUpdateTypeMapping[change.updateType]}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('marketName')}
|
||||
{market?.tradableInstrument?.instrument?.name}
|
||||
@@ -59,10 +73,10 @@ export const ProposalUpdateMarketState = ({
|
||||
{t('marketCode')}
|
||||
{market?.tradableInstrument?.instrument?.code}
|
||||
</KeyValueTableRow>
|
||||
{isTerminate && (
|
||||
{isTerminate && market && (
|
||||
<Row
|
||||
field="termination-price"
|
||||
value={proposal?.terms?.change?.price}
|
||||
value={change?.price}
|
||||
assetSymbol={
|
||||
market?.tradableInstrument?.instrument?.product
|
||||
?.__typename === 'Future' ||
|
||||
|
||||
+40
-80
@@ -1,6 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalVolumeDiscountProgramDetails } from './proposal-volume-discount-program-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
@@ -10,95 +9,56 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockReferralProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateVolumeDiscountProgram',
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
volumeDiscountFactor: '0.05',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '50000',
|
||||
volumeDiscountFactor: '0.1',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '100000',
|
||||
volumeDiscountFactor: '0.15',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '250000',
|
||||
volumeDiscountFactor: '0.2',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
volumeDiscountFactor: '0.25',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
volumeDiscountFactor: '0.3',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1500000',
|
||||
volumeDiscountFactor: '0.35',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '2000000',
|
||||
volumeDiscountFactor: '0.4',
|
||||
},
|
||||
],
|
||||
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
|
||||
windowLength: 7,
|
||||
const mockChange = {
|
||||
__typename: 'UpdateVolumeDiscountProgram' as const,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
volumeDiscountFactor: '0.05',
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '50000',
|
||||
volumeDiscountFactor: '0.1',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '100000',
|
||||
volumeDiscountFactor: '0.15',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '250000',
|
||||
volumeDiscountFactor: '0.2',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
volumeDiscountFactor: '0.25',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
volumeDiscountFactor: '0.3',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1500000',
|
||||
volumeDiscountFactor: '0.35',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '2000000',
|
||||
volumeDiscountFactor: '0.4',
|
||||
},
|
||||
],
|
||||
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
|
||||
windowLength: 7,
|
||||
};
|
||||
|
||||
describe('ProposalVolumeDiscountProgramDetails', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalVolumeDiscountProgramDetails proposal={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-volume-discount-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateVolumeDiscountProgram', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(
|
||||
<ProposalVolumeDiscountProgramDetails proposal={updateMarketProposal} />
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-volume-discount-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateVolumeDiscountProgram',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<ProposalVolumeDiscountProgramDetails proposal={incompleteProposal} />
|
||||
);
|
||||
render(<ProposalVolumeDiscountProgramDetails change={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-volume-discount-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should render relevant fields if present', () => {
|
||||
render(
|
||||
<ProposalVolumeDiscountProgramDetails proposal={mockReferralProposal} />
|
||||
);
|
||||
render(<ProposalVolumeDiscountProgramDetails change={mockChange} />);
|
||||
expect(
|
||||
screen.getByTestId('proposal-volume-discount-program-window-length')
|
||||
).toBeInTheDocument();
|
||||
|
||||
+7
-7
@@ -11,10 +11,10 @@ import {
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type UpdateVolumeDiscountProgramsFragment } from '../../__generated__/Proposals';
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: Proposal | null;
|
||||
change: UpdateVolumeDiscountProgramsFragment | null;
|
||||
}
|
||||
|
||||
export const formatVolumeDiscountFactor = (value: string) => {
|
||||
@@ -22,16 +22,16 @@ export const formatVolumeDiscountFactor = (value: string) => {
|
||||
};
|
||||
|
||||
export const ProposalVolumeDiscountProgramDetails = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (proposal?.terms?.change?.__typename !== 'UpdateVolumeDiscountProgram') {
|
||||
if (change?.__typename !== 'UpdateVolumeDiscountProgram') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers;
|
||||
const windowLength = proposal?.terms?.change?.windowLength;
|
||||
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgramTimestamp;
|
||||
const benefitTiers = change.benefitTiers;
|
||||
const windowLength = change?.windowLength;
|
||||
const endOfProgramTimestamp = change?.endOfProgramTimestamp;
|
||||
|
||||
if (!benefitTiers && !windowLength && !endOfProgramTimestamp) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
// rainbow-ish order
|
||||
const COLOURS = ['red', 'pink', 'orange', 'yellow', 'green', 'blue', 'purple'];
|
||||
|
||||
const getColour = (indicator: number, max = COLOURS.length) => {
|
||||
const available =
|
||||
max < COLOURS.length ? COLOURS.slice(COLOURS.length - max) : COLOURS;
|
||||
const tiers = Object.keys(available).length;
|
||||
let index = Math.abs(indicator - 1);
|
||||
if (indicator >= tiers) {
|
||||
index = index % tiers;
|
||||
}
|
||||
return available[index];
|
||||
};
|
||||
|
||||
export const getStyle = (indicator: number, max = COLOURS.length) =>
|
||||
classNames({
|
||||
'bg-vega-yellow-400 before:bg-vega-yellow-400':
|
||||
'yellow' === getColour(indicator, max),
|
||||
'bg-vega-green-400 before:bg-vega-green-400':
|
||||
'green' === getColour(indicator, max),
|
||||
'bg-vega-blue-400 before:bg-vega-blue-400':
|
||||
'blue' === getColour(indicator, max),
|
||||
'bg-vega-purple-400 before:bg-vega-purple-400':
|
||||
'purple' === getColour(indicator, max),
|
||||
'bg-vega-pink-400 before:bg-vega-pink-400':
|
||||
'pink' === getColour(indicator, max),
|
||||
'bg-vega-orange-400 before:bg-vega-orange-400':
|
||||
'orange' === getColour(indicator, max),
|
||||
'bg-vega-red-400 before:bg-vega-red-400':
|
||||
'red' === getColour(indicator, max),
|
||||
'bg-vega-clight-600 before:bg-vega-clight-600':
|
||||
'none' === getColour(indicator, max),
|
||||
});
|
||||
|
||||
export const getIndicatorStyle = (indicator: number) =>
|
||||
classNames(
|
||||
'rounded-sm text-black inline-block px-1 py-1 font-alpha calt h-8 w-7 text-center',
|
||||
'text-border-1',
|
||||
getStyle(indicator),
|
||||
// Comment below if you want to remove the "chevron"
|
||||
'relative mr-[11px] z-1',
|
||||
'before:absolute before:z-0 before:top-1 before:right-[-11px] before:rounded-sm',
|
||||
"before:w-[22.62px] before:h-[22.62px] before:rotate-45 before:content-['']"
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { getIndicatorStyle } from './colours';
|
||||
|
||||
export const Indicator = ({ indicator }: { indicator: number }) => (
|
||||
<div className={getIndicatorStyle(indicator)}>
|
||||
<span className="absolute top-0 left-0 p-1 w-full text-center z-1">
|
||||
{indicator}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMarketInfoQuery } from '@vegaprotocol/markets';
|
||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const MarketName = ({
|
||||
marketId,
|
||||
truncate,
|
||||
}: {
|
||||
marketId?: string;
|
||||
truncate?: boolean;
|
||||
}) => {
|
||||
const { data } = useMarketInfoQuery({
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
},
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const id = truncate ? truncateMiddle(marketId || '') : marketId;
|
||||
|
||||
return <span>{data?.market?.tradableInstrument.instrument.code || id}</span>;
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
import { ListAsset } from '../list-asset';
|
||||
import { ProposalAssetDetails } from '../proposal-asset-details';
|
||||
import { ProposalMarketChanges } from '../proposal-market-changes';
|
||||
import { ProposalMarketData } from '../proposal-market-data';
|
||||
import { ProposalReferralProgramDetails } from '../proposal-referral-program-details';
|
||||
import {
|
||||
ProposalCancelTransferDetails,
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
|
||||
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
|
||||
import { type ProposalNode } from './proposal-utils';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import { Indicator } from './indicator';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
|
||||
export const ProposalChangeDetails = ({
|
||||
proposal,
|
||||
terms,
|
||||
restData,
|
||||
indicator,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
terms: ProposalTermsFieldsFragment;
|
||||
restData: ProposalNode | null;
|
||||
indicator?: number;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
let details = null;
|
||||
|
||||
switch (terms.change.__typename) {
|
||||
case 'NewAsset': {
|
||||
if (proposal.id && terms.change.source.__typename === 'ERC20') {
|
||||
details = (
|
||||
<div>
|
||||
<ListAsset
|
||||
assetId={proposal.id}
|
||||
withdrawalThreshold={terms.change.source.withdrawThreshold}
|
||||
lifetimeLimit={terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
<ProposalAssetDetails change={terms.change} assetId={proposal.id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
if (proposal.id) {
|
||||
details = (
|
||||
<ProposalAssetDetails
|
||||
change={terms.change}
|
||||
assetId={terms.change.assetId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'NewMarket': {
|
||||
if (proposal.id) {
|
||||
details = <ProposalMarketData proposalId={proposal.id} />;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
if (proposal.id) {
|
||||
details = (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ProposalMarketData proposalId={proposal.id} />
|
||||
<ProposalMarketChanges
|
||||
indicator={indicator}
|
||||
marketId={terms.change.marketId}
|
||||
updateProposalNode={restData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'NewTransfer': {
|
||||
if (proposal.id) {
|
||||
details = <ProposalTransferDetails proposalId={proposal.id} />;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CancelTransfer': {
|
||||
if (proposal.id) {
|
||||
details = <ProposalCancelTransferDetails proposalId={proposal.id} />;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
details = <ProposalUpdateMarketState change={terms.change} />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateReferralProgram': {
|
||||
details = <ProposalReferralProgramDetails change={terms.change} />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateVolumeDiscountProgram': {
|
||||
details = <ProposalVolumeDiscountProgramDetails change={terms.change} />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
if (
|
||||
terms.change.networkParameter.key === 'rewards.vesting.benefitTiers' ||
|
||||
terms.change.networkParameter.key ===
|
||||
'rewards.activityStreak.benefitTiers'
|
||||
) {
|
||||
details = <ProposalUpdateBenefitTiers change={terms.change} />;
|
||||
} else {
|
||||
details = (
|
||||
<div className="mb-4">
|
||||
<SubHeading title={t(terms.change.__typename as string)} />
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey="Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>"
|
||||
values={{
|
||||
key: terms.change.networkParameter.key,
|
||||
value: terms.change.networkParameter.value,
|
||||
}}
|
||||
components={{
|
||||
// @ts-ignore children passed by i18next
|
||||
lozenge: <Lozenge />,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'NewFreeform':
|
||||
case 'NewSpotMarket':
|
||||
case 'UpdateSpotMarket':
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (indicator != null && details != null) {
|
||||
details = (
|
||||
<div className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 mb-3">
|
||||
<div className="w-10">
|
||||
<Indicator indicator={indicator} />
|
||||
</div>
|
||||
<div>{details}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { ENV } from '../../../../config';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type Maybe<T> = T | null | undefined;
|
||||
|
||||
type ProposalState =
|
||||
| 'STATE_UNSPECIFIED'
|
||||
| 'STATE_FAILED'
|
||||
| 'STATE_OPEN'
|
||||
| 'STATE_PASSED'
|
||||
| 'STATE_REJECTED'
|
||||
| 'STATE_DECLINED'
|
||||
| 'STATE_ENACTED'
|
||||
| 'STATE_WAITING_FOR_NODE_VOTE';
|
||||
|
||||
type ProposalType =
|
||||
| 'TYPE_UNSPECIFIED'
|
||||
| 'TYPE_ALL'
|
||||
| 'TYPE_NEW_MARKET'
|
||||
| 'TYPE_UPDATE_MARKET'
|
||||
| 'TYPE_NETWORK_PARAMETERS'
|
||||
| 'TYPE_NEW_ASSET'
|
||||
| 'TYPE_NEW_FREE_FORM'
|
||||
| 'TYPE_UPDATE_ASSET'
|
||||
| 'TYPE_NEW_SPOT_MARKET'
|
||||
| 'TYPE_UPDATE_SPOT_MARKET'
|
||||
| 'TYPE_NEW_TRANSFER'
|
||||
| 'TYPE_CANCEL_TRANSFER'
|
||||
| 'TYPE_UPDATE_MARKET_STATE'
|
||||
| 'TYPE_UPDATE_REFERRAL_PROGRAM'
|
||||
| 'TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM';
|
||||
|
||||
type ProposalNodeType = 'TYPE_SINGLE_OR_UNSPECIFIED' | 'TYPE_BATCH';
|
||||
|
||||
type ProposalData = {
|
||||
id: string;
|
||||
rationale: {
|
||||
description: string;
|
||||
title: string;
|
||||
};
|
||||
state: ProposalState;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
type Terms = {
|
||||
cancelTransfer?: { changes: unknown };
|
||||
enactmentTimestamp: string;
|
||||
newAsset?: { changes: unknown };
|
||||
newFreeform: object;
|
||||
newMarket?: { changes: unknown };
|
||||
newSpotMarket?: { changes: unknown };
|
||||
newTransfer?: { changes: unknown };
|
||||
updateAsset?: { assetId: string; changes: unknown };
|
||||
updateMarket?: { marketId: string; changes: unknown };
|
||||
updateMarketState?: {
|
||||
changes: {
|
||||
marketId: string;
|
||||
price: string;
|
||||
updateType:
|
||||
| 'MARKET_STATE_UPDATE_TYPE_UNSPECIFIED'
|
||||
| 'MARKET_STATE_UPDATE_TYPE_TERMINATE'
|
||||
| 'MARKET_STATE_UPDATE_TYPE_SUSPEND'
|
||||
| 'MARKET_STATE_UPDATE_TYPE_RESUME';
|
||||
};
|
||||
};
|
||||
updateNetworkParameter?: { changes: unknown };
|
||||
updateReferralProgram?: { changes: unknown };
|
||||
updateSpotMarket?: { marketId: string; changes: unknown };
|
||||
updateVolumeDiscountProgram?: { changes: unknown };
|
||||
};
|
||||
|
||||
export type SingleProposalData = ProposalData & {
|
||||
terms: Terms & {
|
||||
closingTimestamp: string;
|
||||
validationTimestamp: string;
|
||||
};
|
||||
};
|
||||
|
||||
type BatchProposalData = ProposalData & {
|
||||
batchTerms: {
|
||||
changes: Terms[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SubProposalData = SingleProposalData & {
|
||||
batchId: string;
|
||||
};
|
||||
|
||||
export type ProposalNode = {
|
||||
proposal: ProposalData;
|
||||
proposalType: ProposalNodeType;
|
||||
proposals: SubProposalData[];
|
||||
};
|
||||
|
||||
type SingleProposalNode = ProposalNode & {
|
||||
proposal: SingleProposalData;
|
||||
proposalType: 'TYPE_SINGLE_OR_UNSPECIFIED';
|
||||
proposals: [];
|
||||
};
|
||||
|
||||
type BatchProposalNode = ProposalNode & {
|
||||
proposal: BatchProposalData;
|
||||
proposalType: 'TYPE_BATCH';
|
||||
};
|
||||
|
||||
export const isProposalNode = (node: unknown): node is ProposalNode =>
|
||||
Boolean(
|
||||
typeof node === 'object' &&
|
||||
node &&
|
||||
'proposal' in node &&
|
||||
typeof node.proposal === 'object' &&
|
||||
node?.proposal &&
|
||||
'id' in node.proposal &&
|
||||
node?.proposal?.id
|
||||
);
|
||||
|
||||
export const isSingleProposalNode = (
|
||||
node: Maybe<ProposalNode>
|
||||
): node is SingleProposalNode =>
|
||||
Boolean(
|
||||
node &&
|
||||
node?.proposalType === 'TYPE_SINGLE_OR_UNSPECIFIED' &&
|
||||
node?.proposal
|
||||
);
|
||||
|
||||
export const isBatchProposalNode = (
|
||||
node: Maybe<ProposalNode>
|
||||
): node is BatchProposalNode =>
|
||||
Boolean(
|
||||
node &&
|
||||
node?.proposalType === 'TYPE_BATCH' &&
|
||||
node?.proposal &&
|
||||
'batchTerms' in node.proposal &&
|
||||
node?.proposals?.length > 0
|
||||
);
|
||||
|
||||
// this includes also batch proposals with `updateMarket`s 👍
|
||||
const PROPOSALS_ENDPOINT = `${ENV.rest}governances?proposalState=:proposalState&proposalType=:proposalType`;
|
||||
|
||||
// this can be queried also by sub proposal id as `proposalId` and it will
|
||||
// return full batch proposal data with all of its sub proposals including
|
||||
// the requested one inside `proposals` array.
|
||||
const PROPOSAL_ENDPOINT = `${ENV.rest}governance?proposalId=:proposalId`;
|
||||
|
||||
export const getProposals = async ({
|
||||
proposalState,
|
||||
proposalType,
|
||||
}: {
|
||||
proposalState: ProposalState;
|
||||
proposalType: ProposalType;
|
||||
}) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
PROPOSALS_ENDPOINT.replace(':proposalState', proposalState).replace(
|
||||
':proposalType',
|
||||
proposalType
|
||||
)
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (
|
||||
data &&
|
||||
'connection' in data &&
|
||||
data.connection &&
|
||||
'edges' in data.connection &&
|
||||
data.connection.edges?.length > 0
|
||||
) {
|
||||
const nodes = compact(
|
||||
data.connection.edges.map((e: { node?: object }) => e?.node)
|
||||
).filter(isProposalNode);
|
||||
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// NOOP - ignore errors
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getProposal = async ({ proposalId }: { proposalId: string }) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
PROPOSAL_ENDPOINT.replace(':proposalId', proposalId)
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && 'data' in data && isProposalNode(data.data)) {
|
||||
return data.data as ProposalNode;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// NOOP - ignore errors
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useFetchProposal = ({ proposalId }: { proposalId?: string }) => {
|
||||
const [data, setData] = useState<ProposalNode | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const cb = async () => {
|
||||
if (!proposalId) return;
|
||||
|
||||
setLoading(true);
|
||||
const data = await getProposal({ proposalId });
|
||||
setLoading(false);
|
||||
if (data) {
|
||||
setData(data);
|
||||
}
|
||||
};
|
||||
cb();
|
||||
}, [proposalId]);
|
||||
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const useFetchProposals = ({
|
||||
proposalState,
|
||||
proposalType,
|
||||
}: {
|
||||
proposalState: ProposalState;
|
||||
proposalType: ProposalType;
|
||||
}) => {
|
||||
const [data, setData] = useState<ProposalNode[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const cb = async () => {
|
||||
setLoading(true);
|
||||
const data = await getProposals({ proposalState, proposalType });
|
||||
setLoading(false);
|
||||
if (data) {
|
||||
setData(data);
|
||||
}
|
||||
};
|
||||
cb();
|
||||
}, [proposalState, proposalType]);
|
||||
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const flatten = (
|
||||
nodes: ProposalNode[]
|
||||
): (SingleProposalData | SubProposalData)[] => {
|
||||
const flattenNodes = [];
|
||||
for (const node of nodes) {
|
||||
if (isSingleProposalNode(node)) {
|
||||
flattenNodes.push(node.proposal);
|
||||
}
|
||||
if (isBatchProposalNode(node)) {
|
||||
for (const sub of node.proposals) {
|
||||
flattenNodes.push(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flattenNodes;
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import { render, screen } from '@testing-library/react';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { Proposal } from './proposal';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { mockNetworkParams } from '../../test-helpers/mocks';
|
||||
import { type Proposal as IProposal } from '../../types';
|
||||
|
||||
jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
@@ -38,6 +37,12 @@ jest.mock('../list-asset', () => ({
|
||||
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
|
||||
}));
|
||||
|
||||
jest.mock('./proposal-change-details', () => ({
|
||||
ProposalChangeDetails: () => (
|
||||
<div data-testid="proposal-change-details"></div>
|
||||
),
|
||||
}));
|
||||
|
||||
const vegaWalletConfig: VegaWalletConfig = {
|
||||
network: 'TESTNET',
|
||||
vegaUrl: 'https://vega.xyz',
|
||||
@@ -56,11 +61,7 @@ const renderComponent = (proposal: IProposal) => {
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<VegaWalletProvider config={vegaWalletConfig}>
|
||||
<Proposal
|
||||
restData={{}}
|
||||
proposal={proposal}
|
||||
networkParams={mockNetworkParams}
|
||||
/>
|
||||
<Proposal restData={null} proposal={proposal} />
|
||||
</VegaWalletProvider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
@@ -99,27 +100,6 @@ it('renders each section', async () => {
|
||||
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-change-details')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders whitelist section if proposal is new asset and source is erc20', async () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
name: 'foo',
|
||||
symbol: 'FOO',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
lifetimeLimit: '1',
|
||||
withdrawThreshold: '100',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
renderComponent(proposal);
|
||||
|
||||
expect(screen.getByTestId('proposal-list-asset')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -5,152 +5,27 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalDescription } from '../proposal-description';
|
||||
import { ProposalChangeTable } from '../proposal-change-table';
|
||||
import { ProposalJson } from '../proposal-json';
|
||||
import { ProposalAssetDetails } from '../proposal-asset-details';
|
||||
import { ProposalReferralProgramDetails } from '../proposal-referral-program-details';
|
||||
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
|
||||
import { UserVote } from '../vote-details';
|
||||
import { ListAsset } from '../list-asset';
|
||||
import Routes from '../../../routes';
|
||||
import { ProposalMarketData } from '../proposal-market-data';
|
||||
import { type MarketInfo } from '@vegaprotocol/markets';
|
||||
import { type AssetQuery } from '@vegaprotocol/assets';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { ProposalMarketChanges } from '../proposal-market-changes';
|
||||
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
|
||||
import { type NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
import { type ProposalNode } from './proposal-utils';
|
||||
import { useVoteSubmit } from '@vegaprotocol/proposals';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import {
|
||||
ProposalCancelTransferDetails,
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
import { type Proposal as IProposal } from '../../types';
|
||||
import { type Proposal as IProposal, type BatchProposal } from '../../types';
|
||||
import { ProposalChangeDetails } from './proposal-change-details';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
export interface ProposalProps {
|
||||
proposal: IProposal;
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
marketData?: MarketInfo | null;
|
||||
parentMarketData?: MarketInfo | null;
|
||||
assetData?: AssetQuery | null;
|
||||
proposal: IProposal | BatchProposal;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
restData: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
originalMarketProposalRestData?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mostRecentlyEnactedAssociatedMarketProposal?: any;
|
||||
restData: ProposalNode | null;
|
||||
}
|
||||
|
||||
export const Proposal = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
restData,
|
||||
marketData,
|
||||
parentMarketData,
|
||||
assetData,
|
||||
originalMarketProposalRestData,
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
}: ProposalProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
export const Proposal = ({ proposal, restData }: ProposalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
|
||||
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
|
||||
|
||||
if (!proposal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let asset = assetData
|
||||
? removePaginationWrapper(assetData.assetsConnection?.edges)[0]
|
||||
: undefined;
|
||||
|
||||
const originalAsset = asset;
|
||||
|
||||
if (proposal.terms.change.__typename === 'UpdateAsset' && asset) {
|
||||
asset = {
|
||||
...asset,
|
||||
quantum: proposal.terms.change.quantum,
|
||||
source: { ...asset.source },
|
||||
};
|
||||
|
||||
if (asset.source.__typename === 'ERC20') {
|
||||
asset.source.lifetimeLimit = proposal.terms.change.source.lifetimeLimit;
|
||||
asset.source.withdrawThreshold =
|
||||
proposal.terms.change.source.withdrawThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
let minVoterBalance = null;
|
||||
|
||||
if (networkParams) {
|
||||
switch (proposal.terms.change.__typename) {
|
||||
case 'UpdateMarket':
|
||||
case 'UpdateMarketState':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
||||
break;
|
||||
case 'NewMarket':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_market_minVoterBalance;
|
||||
break;
|
||||
case 'NewAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_asset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateNetworkParameter':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
||||
break;
|
||||
case 'NewFreeform':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'NewTransfer':
|
||||
// TODO: check minVoterBalance for 'NewTransfer'
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'CancelTransfer':
|
||||
// TODO: check minVoterBalance for 'CancelTransfer'
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateReferralProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_referralProgram_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateVolumeDiscountProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Show governance transfer details only if the GOVERNANCE_TRANSFERS flag is on.
|
||||
const governanceTransferDetails = featureFlags.GOVERNANCE_TRANSFERS && (
|
||||
<>
|
||||
{proposal.terms.change.__typename === 'NewTransfer' && (
|
||||
/** Governance New Transfer Details */
|
||||
<div className="mb-4">
|
||||
<ProposalTransferDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'CancelTransfer' && (
|
||||
/** Governance Cancel Transfer Details */
|
||||
<div className="mb-4">
|
||||
<ProposalCancelTransferDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<section data-testid="proposal">
|
||||
<div className="flex items-center gap-1 mb-6">
|
||||
@@ -181,91 +56,37 @@ export const Proposal = ({
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
||||
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
proposal.terms.change.source.__typename === 'ERC20' &&
|
||||
proposal.id ? (
|
||||
<ListAsset
|
||||
assetId={proposal.id}
|
||||
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
|
||||
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalDescription description={proposal.rationale.description} />
|
||||
</div>
|
||||
|
||||
{marketData && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketData
|
||||
marketData={marketData}
|
||||
parentMarketData={parentMarketData ? parentMarketData : undefined}
|
||||
<div className="mb-4 flex flex-col gap-0">
|
||||
{proposal.__typename === 'Proposal' ? (
|
||||
<ProposalChangeDetails
|
||||
proposal={proposal}
|
||||
terms={proposal.terms}
|
||||
restData={restData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateMarketState' && (
|
||||
<div className="mb-4">
|
||||
<ProposalUpdateMarketState proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateMarket' && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketChanges
|
||||
originalProposal={
|
||||
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
|
||||
?.changes || {}
|
||||
}
|
||||
latestEnactedProposal={
|
||||
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal?.terms
|
||||
?.updateMarket?.changes || {}
|
||||
}
|
||||
updatedProposal={
|
||||
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(proposal.terms.change.__typename === 'NewAsset' ||
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalAssetDetails asset={asset} originalAsset={originalAsset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateReferralProgram' && (
|
||||
<div className="mb-4">
|
||||
<ProposalReferralProgramDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateVolumeDiscountProgram' && (
|
||||
<div className="mb-4">
|
||||
<ProposalVolumeDiscountProgramDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
|
||||
proposal.terms.change.networkParameter.key.slice(-13) ===
|
||||
'.benefitTiers' && (
|
||||
<div className="mb-4">
|
||||
<ProposalUpdateBenefitTiers proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{governanceTransferDetails}
|
||||
) : proposal.__typename === 'BatchProposal' ? (
|
||||
proposal.subProposals?.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<ProposalChangeDetails
|
||||
indicator={i + 1}
|
||||
key={i}
|
||||
proposal={proposal}
|
||||
terms={p.terms}
|
||||
restData={restData}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<UserVote
|
||||
proposal={proposal}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
networkParams?.spam_protection_voting_min_tokens
|
||||
}
|
||||
submit={submit}
|
||||
dialog={Dialog}
|
||||
transaction={transaction}
|
||||
@@ -276,7 +97,7 @@ export const Proposal = ({
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<ProposalJson proposal={restData?.data?.proposal} />
|
||||
<ProposalJson proposal={restData?.proposal as unknown as JsonValue} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
+11
-135
@@ -1,149 +1,25 @@
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { format } from 'date-fns';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
fiveMinutes,
|
||||
fiveHours,
|
||||
fiveDays,
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { type Proposal } from '../../types';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
|
||||
const renderComponent = (
|
||||
proposal: Proposal,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
|
||||
) =>
|
||||
const renderComponent = (id: string) =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsListItemDetails id={id} />
|
||||
</VegaWalletContext.Provider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Proposals list item details', () => {
|
||||
it('Renders proposal state: Enacted', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
format(lastWeek, DATE_FORMAT_DETAILED)
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Passed', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Waiting for node vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 minutes left to vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveMinutes.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 minutes left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 hours left to vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveHours.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 hours left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 days left to vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveDays.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 days left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Rejected', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_REJECTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
rejectionReason:
|
||||
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Invalid future product'
|
||||
it('links to single proposal page', () => {
|
||||
const proposalId = 'proposal-id';
|
||||
renderComponent(proposalId);
|
||||
expect(screen.getByRole('link')).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposalId)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+4
-89
@@ -1,101 +1,16 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import {
|
||||
ProposalRejectionReasonMapping,
|
||||
ProposalState,
|
||||
} from '@vegaprotocol/types';
|
||||
import Routes from '../../../routes';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalsListItemDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
export const ProposalsListItemDetails = ({ id }: { id: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const state = proposal?.state;
|
||||
const nowToEnactmentInHours = differenceInHours(
|
||||
new Date(proposal?.terms.closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
|
||||
let voteDetails: ReactNode;
|
||||
let voteStatus: ReactNode;
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
voteDetails =
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal?.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_PASSED: {
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
voteDetails = (
|
||||
<span className={nowToEnactmentInHours < 6 ? 'text-vega-orange' : ''}>
|
||||
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
|
||||
{t('left to vote')}
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
voteStatus = proposal?.rejectionReason && (
|
||||
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 items-start text-sm">
|
||||
<div className="flex items-center gap-2 text-vega-light-300 mb-2">
|
||||
{voteDetails && <span data-testid="vote-details">{voteDetails}</span>}
|
||||
{voteDetails && voteStatus && <span>·</span>}
|
||||
{voteStatus && <span data-testid="vote-status">{voteStatus}</span>}
|
||||
</div>
|
||||
|
||||
{proposal?.id && (
|
||||
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link to={`${Routes.PROPOSALS}/${id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -2,10 +2,10 @@ import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
interface ProposalsListItemProps {
|
||||
proposal?: Proposal | null;
|
||||
proposal?: Proposal | BatchProposal;
|
||||
}
|
||||
|
||||
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
@@ -16,7 +16,7 @@ export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
<li id={proposal.id} data-testid="proposals-list-item">
|
||||
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
||||
<ProposalHeader proposal={proposal} voteState={voteState} />
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
<ProposalsListItemDetails id={proposal.id} />
|
||||
</RoundedWrapper>
|
||||
</li>
|
||||
);
|
||||
|
||||
+14
-12
@@ -3,14 +3,12 @@ import {
|
||||
generateProtocolUpgradeProposal,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { ProposalsList } from './proposals-list';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
@@ -20,6 +18,16 @@ import {
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
jest.mock('../proposals-list-item', () => ({
|
||||
ProposalsListItem: ({ proposal }: { proposal: { id: string } }) => (
|
||||
<div data-testid="proposals-list-item" id={proposal.id} />
|
||||
),
|
||||
}));
|
||||
|
||||
const openProposalClosesNextMonth = generateProposal({
|
||||
id: 'proposal1',
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -69,12 +77,10 @@ const renderComponent = (
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsList
|
||||
proposals={proposals}
|
||||
protocolUpgradeProposals={protocolUpgradeProposals || []}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<ProposalsList
|
||||
proposals={proposals}
|
||||
protocolUpgradeProposals={protocolUpgradeProposals || []}
|
||||
/>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
@@ -88,10 +94,6 @@ afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
describe('Proposals list', () => {
|
||||
it('Render a page title and link to the make proposal form', async () => {
|
||||
render(renderComponent([]));
|
||||
|
||||
@@ -11,19 +11,20 @@ import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type BatchProposal, type Proposal } from '../../types';
|
||||
|
||||
type Proposals = Array<Proposal | BatchProposal>;
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Proposal[];
|
||||
proposals: Proposals;
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
lastBlockHeight?: string;
|
||||
}
|
||||
|
||||
interface SortedProposalsProps {
|
||||
open: Proposal[];
|
||||
closed: Proposal[];
|
||||
open: Proposals;
|
||||
closed: Proposals;
|
||||
}
|
||||
|
||||
interface SortedProtocolUpgradeProposalsProps {
|
||||
@@ -31,15 +32,26 @@ interface SortedProtocolUpgradeProposalsProps {
|
||||
closed: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}
|
||||
|
||||
export const orderByDate = (arr: Proposal[]) =>
|
||||
export const orderByDate = (arr: Proposals) =>
|
||||
orderBy(
|
||||
arr,
|
||||
[
|
||||
(p) =>
|
||||
p?.terms?.enactmentDatetime
|
||||
? new Date(p?.terms?.enactmentDatetime).getTime()
|
||||
: // has to be defaulted to 0 because new Date(null).getTime() -> NaN which is first when ordered
|
||||
new Date(p?.terms?.closingDatetime || 0).getTime(),
|
||||
(p) => {
|
||||
if (p.__typename === 'BatchProposal') {
|
||||
// Batch proposals can have different enactment dates, this could be improved by ordering
|
||||
// by soonest enactment date in the batch
|
||||
return new Date(p.batchTerms?.closingDatetime || p.datetime);
|
||||
}
|
||||
|
||||
if (p.__typename === 'Proposal') {
|
||||
return p?.terms?.enactmentDatetime
|
||||
? new Date(p?.terms?.enactmentDatetime).getTime()
|
||||
: // has to be defaulted to 0 because new Date(null).getTime() -> NaN which is first when ordered
|
||||
new Date(p?.terms?.closingDatetime || 0).getTime();
|
||||
}
|
||||
|
||||
throw new Error('invalid proposal');
|
||||
},
|
||||
(p) => new Date(p?.datetime).getTime(),
|
||||
],
|
||||
['asc', 'asc']
|
||||
@@ -76,18 +88,40 @@ export const ProposalsList = ({
|
||||
|
||||
const sortedProposals: SortedProposalsProps = useMemo(() => {
|
||||
const initialSorting = proposals.reduce(
|
||||
(acc: SortedProposalsProps, proposal) => {
|
||||
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
|
||||
acc.open.push(proposal);
|
||||
} else {
|
||||
acc.closed.push(proposal);
|
||||
(acc, proposal) => {
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
|
||||
acc.open.push(proposal);
|
||||
} else {
|
||||
acc.closed.push(proposal);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal') {
|
||||
if (
|
||||
// this could be improved by sorting by soonest enactment date of all the
|
||||
// sub proposals
|
||||
isFuture(
|
||||
new Date(
|
||||
proposal.batchTerms?.closingDatetime || proposal.datetime
|
||||
)
|
||||
)
|
||||
) {
|
||||
acc.open.push(proposal);
|
||||
} else {
|
||||
acc.closed.push(proposal);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
open: [],
|
||||
closed: [],
|
||||
}
|
||||
} as SortedProposalsProps
|
||||
);
|
||||
return {
|
||||
open:
|
||||
@@ -121,7 +155,7 @@ export const ProposalsList = ({
|
||||
};
|
||||
}, [protocolUpgradeProposals, lastBlockHeight]);
|
||||
|
||||
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
|
||||
const filterPredicate = (p: Proposal | BatchProposal) =>
|
||||
p?.id?.includes(filterString) ||
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
@@ -172,7 +206,7 @@ export const ProposalsList = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
|
||||
<section className="-mx-4 p-4 mb-8 bg-vega-cdark-900 rounded-l-sm">
|
||||
<SubHeading title={t('openProposals')} />
|
||||
|
||||
{sortedProposals.open.length > 0 ||
|
||||
|
||||
+9
-46
@@ -1,17 +1,8 @@
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { RejectedProposalsList } from './rejected-proposals-list';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
nextWeek,
|
||||
lastMonth,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { nextWeek, lastMonth } from '../../test-helpers/mocks';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
const rejectedProposalClosesNextWeek = generateProposal({
|
||||
@@ -36,31 +27,15 @@ const rejectedProposalClosedLastMonth = generateProposal({
|
||||
});
|
||||
|
||||
const renderComponent = (proposals: Proposal[]) => (
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<RejectedProposalsList proposals={proposals} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
<RejectedProposalsList proposals={proposals} />
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
jest.mock('../proposals-list-item', () => ({
|
||||
ProposalsListItem: () => <div data-testid="proposals-list-item" />,
|
||||
}));
|
||||
|
||||
describe('Rejected proposals list', () => {
|
||||
it('Renders a list of proposals', async () => {
|
||||
it('Renders a list of proposals', () => {
|
||||
render(
|
||||
renderComponent([
|
||||
rejectedProposalClosedLastMonth,
|
||||
@@ -68,25 +43,13 @@ describe('Rejected proposals list', () => {
|
||||
])
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const rejectedProposals = within(
|
||||
screen.getByTestId('rejected-proposals')
|
||||
);
|
||||
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
|
||||
'proposals-list-item'
|
||||
);
|
||||
expect(rejectedProposalsItems).toHaveLength(2);
|
||||
});
|
||||
expect(screen.getAllByTestId('proposals-list-item')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Displays text when there are no proposals', async () => {
|
||||
render(renderComponent([]));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('rejected-proposals')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+3
-3
@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { ProposalsListItem } from '../proposals-list-item';
|
||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type BatchProposal, type Proposal } from '../../types';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Proposal[];
|
||||
proposals: Array<Proposal | BatchProposal>;
|
||||
}
|
||||
|
||||
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [filterString, setFilterString] = useState('');
|
||||
|
||||
const filterPredicate = (p: Proposal) =>
|
||||
const filterPredicate = (p: Proposal | BatchProposal) =>
|
||||
p?.id?.includes(filterString) ||
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
|
||||
+26
-22
@@ -64,7 +64,7 @@ describe('VoteBreakdown', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('Renders majority reached', () => {
|
||||
it('Renders majority reached', async () => {
|
||||
const yesVotes = 100;
|
||||
const noVotes = 0;
|
||||
|
||||
@@ -82,10 +82,10 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('token-majority-met')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('token-majority-met')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders majority not reached', () => {
|
||||
it('Renders majority not reached', async () => {
|
||||
const yesVotes = 20;
|
||||
const noVotes = 80;
|
||||
|
||||
@@ -103,10 +103,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('token-majority-not-met')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders participation reached', () => {
|
||||
it('Renders participation reached', async () => {
|
||||
const yesVotes = 1000;
|
||||
const noVotes = 0;
|
||||
|
||||
@@ -124,10 +126,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('token-participation-met')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('token-participation-met')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders participation not reached', () => {
|
||||
it('Renders participation not reached', async () => {
|
||||
const yesVotes = 0;
|
||||
const noVotes = 0;
|
||||
|
||||
@@ -145,11 +149,11 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('token-participation-not-met')
|
||||
await screen.findByTestId('token-participation-not-met')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', async () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -170,12 +174,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
|
||||
'Currently expected to pass by liquidity vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', async () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -196,12 +200,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
|
||||
'Currently expected to pass by token vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to fail', async () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -222,12 +226,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
|
||||
'Currently expected to fail'
|
||||
);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - token majority', () => {
|
||||
it('Progress bar displays status - token majority', async () => {
|
||||
const yesVotes = 80;
|
||||
const noVotes = 20;
|
||||
|
||||
@@ -246,13 +250,13 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('token-majority-progress');
|
||||
const element = await screen.findByTestId('token-majority-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
expect(style.width).toBe(`${yesVotes}%`);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - token participation', () => {
|
||||
it('Progress bar displays status - token participation', async () => {
|
||||
const yesVotes = 40;
|
||||
const noVotes = 20;
|
||||
const totalVotes = yesVotes + noVotes;
|
||||
@@ -274,13 +278,13 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('token-participation-progress');
|
||||
const element = await screen.findByTestId('token-participation-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
expect(style.width).toBe(`${expectedProgress}%`);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - LP majority', () => {
|
||||
it('Progress bar displays status - LP majority', async () => {
|
||||
const yesVotesLP = 0.8;
|
||||
const noVotesLP = 0.2;
|
||||
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
|
||||
@@ -307,12 +311,12 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('lp-majority-progress');
|
||||
const element = await screen.findByTestId('lp-majority-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
expect(style.width).toBe(`${expectedProgress}%`);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - LP participation', () => {
|
||||
it('Progress bar displays status - LP participation', async () => {
|
||||
const yesVotesLP = 400;
|
||||
const noVotesLP = 600;
|
||||
const totalVotesLP = yesVotesLP + noVotesLP;
|
||||
@@ -341,7 +345,7 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('lp-participation-progress');
|
||||
const element = await screen.findByTestId('lp-participation-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
expect(style.width).toBe(`${expectedProgress}%`);
|
||||
});
|
||||
|
||||
+401
-126
@@ -1,13 +1,22 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import countBy from 'lodash/countBy';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
import {
|
||||
type ProposalTermsFieldsFragment,
|
||||
type VoteFieldsFragment,
|
||||
} from '../../__generated__/Proposals';
|
||||
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
|
||||
import { MarketName } from '../proposal/market-name';
|
||||
import { Indicator } from '../proposal/indicator';
|
||||
|
||||
export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
||||
<CompactNumber
|
||||
@@ -18,10 +27,6 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
interface VoteBreakdownProps {
|
||||
proposal: Proposal;
|
||||
}
|
||||
|
||||
interface VoteProgressProps {
|
||||
percentageFor: BigNumber;
|
||||
colourfulBg?: boolean;
|
||||
@@ -36,8 +41,9 @@ const VoteProgress = ({
|
||||
children,
|
||||
}: VoteProgressProps) => {
|
||||
const containerClasses = classNames(
|
||||
'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden',
|
||||
colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400'
|
||||
'relative h-2 rounded-md overflow-hidden',
|
||||
// 'border border-vega-dark-300',
|
||||
colourfulBg ? 'bg-vega-red' : 'bg-vega-dark-200'
|
||||
);
|
||||
|
||||
const progressClasses = classNames(
|
||||
@@ -46,17 +52,19 @@ const VoteProgress = ({
|
||||
);
|
||||
|
||||
const textClasses = classNames(
|
||||
'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black'
|
||||
'w-full flex items-center justify-start text-white text-sm pb-1'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div
|
||||
className={progressClasses}
|
||||
style={{ width: `${percentageFor}%` }}
|
||||
data-testid={testId}
|
||||
></div>
|
||||
<div>
|
||||
<div className={textClasses}>{children}</div>
|
||||
<div className={containerClasses}>
|
||||
<div
|
||||
className={progressClasses}
|
||||
style={{ width: `${percentageFor}%` }}
|
||||
data-testid={testId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -75,14 +83,22 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
|
||||
<div data-testid={testId}>
|
||||
{reached ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon name="tick" size={4} />
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
className="text-vega-green"
|
||||
size={20}
|
||||
/>
|
||||
<span>
|
||||
{threshold.toString()}% {text} {t('met')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon name="cross" size={4} />
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CROSS}
|
||||
className="text-vega-red"
|
||||
size={20}
|
||||
/>
|
||||
<span>
|
||||
{threshold.toString()}% {text} {t('not met')}
|
||||
</span>
|
||||
@@ -92,7 +108,254 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
export const VoteBreakdown = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
return <VoteBreakdownNormal proposal={proposal} />;
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal') {
|
||||
return <VoteBreakdownBatch proposal={proposal} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
|
||||
const [fullBreakdown, setFullBreakdown] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const voteInfo = useBatchVoteInformation({
|
||||
terms: compact(
|
||||
proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : []
|
||||
),
|
||||
votes: proposal.votes,
|
||||
});
|
||||
|
||||
if (!voteInfo) return null;
|
||||
|
||||
const batchWillPass = voteInfo.every((i) => i.willPass);
|
||||
|
||||
const passingCount = countBy(voteInfo, (v) => v.willPass);
|
||||
|
||||
if (proposal.state === ProposalState.STATE_OPEN) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{batchWillPass ? (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
className="text-vega-green"
|
||||
size={20}
|
||||
/>
|
||||
{t(
|
||||
'Currently expected to pass: conditions met for {{count}} of {{total}} proposals',
|
||||
{
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CROSS}
|
||||
className="text-vega-red"
|
||||
size={20}
|
||||
/>
|
||||
{t(
|
||||
'Currently expected to fail: {{count}} of {{total}} proposals are passing',
|
||||
{
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => setFullBreakdown((x) => !x)}
|
||||
>
|
||||
{fullBreakdown ? 'Hide vote breakdown' : 'Show vote breakdown'}
|
||||
</button>
|
||||
</div>
|
||||
{fullBreakdown && (
|
||||
<div>
|
||||
{proposal.subProposals?.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<VoteBreakdownBatchSubProposal
|
||||
indicator={i + 1}
|
||||
key={i}
|
||||
proposal={proposal}
|
||||
votes={proposal.votes}
|
||||
terms={p.terms}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
proposal.state === ProposalState.STATE_DECLINED ||
|
||||
proposal.state === ProposalState.STATE_PASSED
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{batchWillPass ? (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
className="text-vega-green"
|
||||
size={20}
|
||||
/>
|
||||
{t(
|
||||
'Proposal passed: conditions met for {{count}} of {{total}} proposals',
|
||||
{
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CROSS}
|
||||
className="text-vega-red"
|
||||
size={20}
|
||||
/>
|
||||
{t('Proposal failed: {{count}} of {{total}} proposals passed', {
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => setFullBreakdown((x) => !x)}
|
||||
>
|
||||
{fullBreakdown ? 'Hide vote breakdown' : 'Show vote breakdown'}
|
||||
</button>
|
||||
</div>
|
||||
{fullBreakdown && (
|
||||
<div>
|
||||
{proposal.subProposals?.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<VoteBreakdownBatchSubProposal
|
||||
indicator={i + 1}
|
||||
key={i}
|
||||
proposal={proposal}
|
||||
votes={proposal.votes}
|
||||
terms={p.terms}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const VoteBreakdownBatchSubProposal = ({
|
||||
proposal,
|
||||
votes,
|
||||
terms,
|
||||
indicator,
|
||||
}: {
|
||||
proposal: BatchProposal;
|
||||
votes: VoteFieldsFragment;
|
||||
terms: ProposalTermsFieldsFragment;
|
||||
indicator?: number;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const voteInfo = useVoteInformation({
|
||||
votes,
|
||||
terms,
|
||||
});
|
||||
|
||||
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
|
||||
|
||||
let marketId = undefined;
|
||||
if (terms?.change?.__typename === 'UpdateMarket') {
|
||||
marketId = terms.change.marketId;
|
||||
}
|
||||
if (terms?.change?.__typename === 'UpdateMarketState') {
|
||||
marketId = terms.change.market.id;
|
||||
}
|
||||
|
||||
const marketName = marketId ? (
|
||||
<>
|
||||
: <MarketName marketId={marketId} />
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const indicatorElement = indicator && <Indicator indicator={indicator} />;
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{indicatorElement}
|
||||
<h4>
|
||||
{t(terms.change.__typename)} {marketName}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="rounded-sm bg-vega-dark-100 p-3">
|
||||
<VoteBreakDownUI
|
||||
voteInfo={voteInfo}
|
||||
isProposalOpen={isProposalOpen}
|
||||
isUpdateMarket={isUpdateMarket}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VoteBreakdownNormal = ({ proposal }: { proposal: Proposal }) => {
|
||||
const voteInfo = useVoteInformation({
|
||||
votes: proposal.votes,
|
||||
terms: proposal.terms,
|
||||
});
|
||||
|
||||
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<VoteBreakDownUI
|
||||
voteInfo={voteInfo}
|
||||
isProposalOpen={isProposalOpen}
|
||||
isUpdateMarket={isUpdateMarket}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VoteBreakDownUI = ({
|
||||
voteInfo,
|
||||
isProposalOpen,
|
||||
isUpdateMarket,
|
||||
}: {
|
||||
voteInfo: ReturnType<typeof useVoteInformation>;
|
||||
isProposalOpen: boolean;
|
||||
isUpdateMarket: boolean;
|
||||
}) => {
|
||||
const defaultDP = 2;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!voteInfo) return null;
|
||||
|
||||
const {
|
||||
totalTokensPercentage,
|
||||
participationMet,
|
||||
@@ -101,7 +364,6 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
noPercentage,
|
||||
noLPPercentage,
|
||||
yesPercentage,
|
||||
yesLPPercentage,
|
||||
yesTokens,
|
||||
noTokens,
|
||||
totalEquityLikeShareWeight,
|
||||
@@ -114,12 +376,9 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
majorityLPMet,
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
} = useVoteInformation({ proposal });
|
||||
lpVoteWeight,
|
||||
} = voteInfo;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const defaultDP = 2;
|
||||
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
|
||||
const participationThresholdProgress = BigNumber.min(
|
||||
totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100),
|
||||
new BigNumber(100)
|
||||
@@ -142,127 +401,52 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
'flex justify-between flex-wrap gap-6'
|
||||
);
|
||||
const sectionClasses = classNames('min-w-[300px] flex-1 flex-grow');
|
||||
const headingClasses = classNames('mb-2 text-vega-dark-400');
|
||||
const headingClasses = classNames('mb-2 text-sm text-white font-bold');
|
||||
const progressDetailsClasses = classNames(
|
||||
'flex justify-between flex-wrap mt-2 text-sm'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div>
|
||||
{isProposalOpen && (
|
||||
<div
|
||||
data-testid="vote-status"
|
||||
className="flex items-center gap-1 mb-2 text-bold"
|
||||
className="flex items-center gap-2 mb-2 text-bold"
|
||||
>
|
||||
<span>
|
||||
{willPass ? (
|
||||
<Icon name="tick" size={5} className="text-vega-green" />
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
size={20}
|
||||
className="text-vega-green"
|
||||
/>
|
||||
) : (
|
||||
<Icon name="cross" size={5} className="text-vega-pink" />
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CROSS}
|
||||
size={20}
|
||||
className="text-vega-red"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span>{t('currentlySetTo')} </span>
|
||||
{willPass ? (
|
||||
<span>
|
||||
<span className="text-vega-green">{t('pass')}</span>
|
||||
<p className="m-0">
|
||||
<Trans
|
||||
i18nKey={'Currently expected to <0>pass</0>'}
|
||||
components={[<span className="text-vega-green" />]}
|
||||
/>
|
||||
{isUpdateMarket && <span> {updateMarketVotePassMethod}</span>}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<span className="text-vega-pink">{t('fail')}</span>
|
||||
<p className="m-0">
|
||||
<Trans
|
||||
i18nKey={'Currently expected to <0>fail</0>'}
|
||||
components={[<span className="text-vega-red" />]}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUpdateMarket && (
|
||||
<div className="mb-4">
|
||||
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
|
||||
<div className={sectionWrapperClasses}>
|
||||
<section
|
||||
className={sectionClasses}
|
||||
data-testid="lp-majority-breakdown"
|
||||
>
|
||||
<VoteProgress
|
||||
percentageFor={yesLPPercentage}
|
||||
colourfulBg={true}
|
||||
testId="lp-majority-progress"
|
||||
>
|
||||
<Status
|
||||
reached={majorityLPMet}
|
||||
threshold={requiredMajorityLPPercentage}
|
||||
text={t('majorityThreshold')}
|
||||
testId={
|
||||
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
|
||||
}
|
||||
/>
|
||||
</VoteProgress>
|
||||
|
||||
<div className={progressDetailsClasses}>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesFor')}:</span>
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{yesLPPercentage.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesAgainst')}:</span>
|
||||
<span>
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{noLPPercentage.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
className={sectionClasses}
|
||||
data-testid="lp-participation-breakdown"
|
||||
>
|
||||
<VoteProgress
|
||||
percentageFor={
|
||||
lpParticipationThresholdProgress || new BigNumber(0)
|
||||
}
|
||||
testId="lp-participation-progress"
|
||||
>
|
||||
<Status
|
||||
reached={participationLPMet}
|
||||
threshold={requiredParticipationLP || new BigNumber(1)}
|
||||
text={t('participationThreshold')}
|
||||
testId={
|
||||
participationLPMet
|
||||
? 'lp-participation-met'
|
||||
: 'lp-participation-not-met'
|
||||
}
|
||||
/>
|
||||
</VoteProgress>
|
||||
|
||||
<div className="flex mt-2 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
|
||||
<Tooltip
|
||||
description={formatNumber(
|
||||
totalEquityLikeShareWeight,
|
||||
defaultDP
|
||||
)}
|
||||
>
|
||||
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>}
|
||||
<div className={sectionWrapperClasses}>
|
||||
<section
|
||||
@@ -362,6 +546,97 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/** Liquidity provider vote */}
|
||||
{isUpdateMarket && (
|
||||
<div className="mt-3">
|
||||
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
|
||||
<div className={sectionWrapperClasses}>
|
||||
<section
|
||||
className={sectionClasses}
|
||||
data-testid="lp-majority-breakdown"
|
||||
>
|
||||
<VoteProgress
|
||||
percentageFor={lpVoteWeight}
|
||||
colourfulBg={true}
|
||||
testId="lp-majority-progress"
|
||||
>
|
||||
<Status
|
||||
reached={majorityLPMet}
|
||||
threshold={requiredMajorityLPPercentage}
|
||||
text={t('majorityThreshold')}
|
||||
testId={
|
||||
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
|
||||
}
|
||||
/>
|
||||
</VoteProgress>
|
||||
|
||||
<div className={progressDetailsClasses}>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesFor')}:</span>
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{lpVoteWeight.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{lpVoteWeight.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesAgainst')}:</span>
|
||||
<span>
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{noLPPercentage.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
className={sectionClasses}
|
||||
data-testid="lp-participation-breakdown"
|
||||
>
|
||||
<VoteProgress
|
||||
percentageFor={
|
||||
lpParticipationThresholdProgress || new BigNumber(0)
|
||||
}
|
||||
testId="lp-participation-progress"
|
||||
>
|
||||
<Status
|
||||
reached={participationLPMet}
|
||||
threshold={requiredParticipationLP || new BigNumber(1)}
|
||||
text={t('participationThreshold')}
|
||||
testId={
|
||||
participationLPMet
|
||||
? 'lp-participation-met'
|
||||
: 'lp-participation-not-met'
|
||||
}
|
||||
/>
|
||||
</VoteProgress>
|
||||
|
||||
<div className="flex mt-2 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
|
||||
<Tooltip
|
||||
description={formatNumber(
|
||||
totalEquityLikeShareWeight,
|
||||
defaultDP
|
||||
)}
|
||||
>
|
||||
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,12 +8,10 @@ import { SubHeading } from '../../../../components/heading';
|
||||
import { type VoteValue } from '@vegaprotocol/types';
|
||||
import { type DialogProps, type VegaTxState } from '@vegaprotocol/proposals';
|
||||
import { type VoteState } from './use-user-vote';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
interface UserVoteProps {
|
||||
proposal: Proposal;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
proposal: Proposal | BatchProposal;
|
||||
transaction: VegaTxState | null;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
@@ -23,8 +21,6 @@ interface UserVoteProps {
|
||||
|
||||
export const UserVote = ({
|
||||
proposal,
|
||||
minVoterBalance,
|
||||
spamProtectionMinTokens,
|
||||
submit,
|
||||
transaction,
|
||||
dialog,
|
||||
@@ -46,12 +42,17 @@ export const UserVote = ({
|
||||
{pubKey ? (
|
||||
proposal && (
|
||||
<VoteButtonsContainer
|
||||
changeType={
|
||||
proposal.__typename === 'BatchProposal'
|
||||
? // @ts-ignore should not be null/undefined
|
||||
proposal.subProposals[0]?.terms?.change.__typename
|
||||
: // @ts-ignore should not be null/undefined
|
||||
proposal.terms?.change.__typename
|
||||
}
|
||||
voteState={voteState}
|
||||
voteDatetime={voteDatetime}
|
||||
proposalState={proposal.state}
|
||||
proposalId={proposal.id ?? ''}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={spamProtectionMinTokens}
|
||||
className="flex"
|
||||
submit={submit}
|
||||
transaction={transaction}
|
||||
|
||||
@@ -19,14 +19,18 @@ import { VoteTransactionDialog } from './vote-transaction-dialog';
|
||||
import { useVoteButtonsQuery } from './__generated__/Stake';
|
||||
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
|
||||
import { filterAcceptableGraphqlErrors } from '../../../../lib/party';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { type ProposalChangeType } from '../../types';
|
||||
|
||||
interface VoteButtonsContainerProps {
|
||||
changeType: ProposalChangeType;
|
||||
voteState: VoteState | null;
|
||||
voteDatetime: Date | null;
|
||||
proposalId: string | null;
|
||||
proposalState: ProposalState;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
transaction: VegaTxState | null;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
@@ -35,20 +39,94 @@ interface VoteButtonsContainerProps {
|
||||
|
||||
export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
|
||||
const { data, loading, error } = useVoteButtonsQuery({
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const { params: networkParams } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
|
||||
NetworkParams.governance_proposal_asset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
|
||||
NetworkParams.governance_proposal_freeform_minVoterBalance,
|
||||
NetworkParams.governance_proposal_referralProgram_minVoterBalance,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance,
|
||||
NetworkParams.governance_proposal_transfer_minVoterBalance,
|
||||
NetworkParams.spam_protection_voting_min_tokens,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_transfer_requiredMajority,
|
||||
]);
|
||||
|
||||
let minVoterBalance = null;
|
||||
|
||||
if (networkParams) {
|
||||
switch (props.changeType) {
|
||||
case 'UpdateMarket':
|
||||
case 'UpdateMarketState':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
||||
break;
|
||||
case 'NewMarket':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_market_minVoterBalance;
|
||||
break;
|
||||
case 'NewAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_asset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateNetworkParameter':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
||||
break;
|
||||
case 'NewFreeform':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'CancelTransfer':
|
||||
case 'NewTransfer':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_transfer_requiredMajority;
|
||||
break;
|
||||
case 'UpdateReferralProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_referralProgram_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateVolumeDiscountProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const filteredErrors = filterAcceptableGraphqlErrors(error);
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={filteredErrors} data={data}>
|
||||
<VoteButtons
|
||||
{...props}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
networkParams.spam_protection_voting_min_tokens
|
||||
}
|
||||
currentStakeAvailable={toBigNum(
|
||||
data?.party?.stakingSummary.currentStakeAvailable || 0,
|
||||
decimals
|
||||
@@ -58,8 +136,17 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface VoteButtonsProps extends VoteButtonsContainerProps {
|
||||
interface VoteButtonsProps {
|
||||
voteState: VoteState | null;
|
||||
voteDatetime: Date | null;
|
||||
proposalId: string | null;
|
||||
proposalState: ProposalState;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
transaction: VegaTxState | null;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
currentStakeAvailable: BigNumber;
|
||||
minVoterBalance: string | null;
|
||||
spamProtectionMinTokens: string | null;
|
||||
}
|
||||
|
||||
export const VoteButtons = ({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { useProposalNetworkParams } from './use-proposal-network-params';
|
||||
import { generateProposal } from '../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
...jest.requireActual('@vegaprotocol/network-parameters'),
|
||||
@@ -29,118 +28,84 @@ jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
|
||||
describe('use-proposal-network-params', () => {
|
||||
it('returns the correct params for an update market proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
} = renderHook(() => useProposalNetworkParams());
|
||||
|
||||
const expectedObj = {
|
||||
requiredMajority: expect.any(BigNumber),
|
||||
requiredMajorityLP: expect.any(BigNumber),
|
||||
requiredParticipation: expect.any(BigNumber),
|
||||
requiredParticipationLP: expect.any(BigNumber),
|
||||
};
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.1',
|
||||
requiredMajorityLP: '0.2',
|
||||
requiredParticipation: new BigNumber(0.15),
|
||||
requiredParticipationLP: new BigNumber(0.25),
|
||||
NewMarket: expectedObj,
|
||||
NewSpotMarket: expectedObj,
|
||||
UpdateMarket: expectedObj,
|
||||
UpdateMarketState: expectedObj,
|
||||
UpdateSpotMarket: expectedObj,
|
||||
UpdateNetworkParameter: expectedObj,
|
||||
NewAsset: expectedObj,
|
||||
UpdateAsset: expectedObj,
|
||||
NewFreeform: expectedObj,
|
||||
UpdateReferralProgram: expectedObj,
|
||||
UpdateVolumeDiscountProgram: expectedObj,
|
||||
NewTransfer: expectedObj,
|
||||
CancelTransfer: expectedObj,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct params for a market proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('returns the correct values for the proposal change type', () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
} = renderHook(() => useProposalNetworkParams());
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.3',
|
||||
requiredParticipation: new BigNumber(0.35),
|
||||
});
|
||||
});
|
||||
expect(current?.UpdateMarket.requiredMajority.toString()).toEqual('0.1');
|
||||
expect(current?.UpdateMarket.requiredMajorityLP.toString()).toEqual('0.2');
|
||||
expect(current?.UpdateMarket.requiredParticipation.toString()).toEqual(
|
||||
'0.15'
|
||||
);
|
||||
expect(current?.UpdateMarket.requiredParticipationLP.toString()).toEqual(
|
||||
'0.25'
|
||||
);
|
||||
|
||||
it('returns the correct params for an asset proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(current?.NewMarket.requiredMajority.toString()).toEqual('0.3');
|
||||
expect(current?.NewMarket.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.NewMarket.requiredParticipation.toString()).toEqual('0.35');
|
||||
expect(current?.NewMarket.requiredParticipationLP.toString()).toEqual('0');
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
expect(current?.NewAsset.requiredMajority.toString()).toEqual('0.4');
|
||||
expect(current?.NewAsset.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.NewAsset.requiredParticipation.toString()).toEqual('0.45');
|
||||
expect(current?.NewAsset.requiredParticipationLP.toString()).toEqual('0');
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.4',
|
||||
requiredParticipation: new BigNumber(0.45),
|
||||
});
|
||||
});
|
||||
expect(current?.UpdateAsset.requiredMajority.toString()).toEqual('0.5');
|
||||
expect(current?.UpdateAsset.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.UpdateAsset.requiredParticipation.toString()).toEqual(
|
||||
'0.55'
|
||||
);
|
||||
|
||||
it('returns the correct params for an update asset proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateAsset',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(current?.UpdateNetworkParameter.requiredMajority.toString()).toEqual(
|
||||
'0.6'
|
||||
);
|
||||
expect(
|
||||
current?.UpdateNetworkParameter.requiredMajorityLP.toString()
|
||||
).toEqual('0');
|
||||
expect(
|
||||
current?.UpdateNetworkParameter.requiredParticipation.toString()
|
||||
).toEqual('0.65');
|
||||
expect(
|
||||
current?.UpdateNetworkParameter.requiredParticipationLP.toString()
|
||||
).toEqual('0');
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.5',
|
||||
requiredParticipation: new BigNumber(0.55),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct params for a network params proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.6',
|
||||
requiredParticipation: new BigNumber(0.65),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct params for a freeform proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.7',
|
||||
requiredParticipation: new BigNumber(0.75),
|
||||
});
|
||||
expect(current?.NewFreeform.requiredMajority.toString()).toEqual('0.7');
|
||||
expect(current?.NewFreeform.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.NewFreeform.requiredParticipation.toString()).toEqual(
|
||||
'0.75'
|
||||
);
|
||||
expect(current?.NewFreeform.requiredParticipationLP.toString()).toEqual(
|
||||
'0'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,35 +3,33 @@ import {
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { type Proposal } from '../types';
|
||||
import { type ProposalChangeType } from '../types';
|
||||
|
||||
export const useProposalNetworkParams = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipationLP,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_market_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_asset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredParticipation,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredMajority,
|
||||
]);
|
||||
const REQUIRED_PARAMS = [
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipationLP,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_market_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_asset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredParticipation,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredMajority,
|
||||
];
|
||||
|
||||
export const useProposalNetworkParams = () => {
|
||||
const { params } = useNetworkParams(REQUIRED_PARAMS);
|
||||
|
||||
const fallback = {
|
||||
requiredMajority: new BigNumber(1),
|
||||
@@ -41,86 +39,152 @@ export const useProposalNetworkParams = ({
|
||||
};
|
||||
|
||||
if (!params) {
|
||||
return fallback;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (proposal?.terms.change.__typename) {
|
||||
case 'UpdateMarket':
|
||||
case 'UpdateMarketState':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateMarket_requiredMajority,
|
||||
requiredMajorityLP:
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP
|
||||
),
|
||||
};
|
||||
case 'UpdateNetworkParameter':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateNetParam_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateNetParam_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewAsset':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_asset_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_asset_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'UpdateAsset':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateAsset_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateAsset_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewMarket':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_market_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_market_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewFreeform':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_freeform_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_freeform_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'UpdateReferralProgram':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_referralProgram_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_referralProgram_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'UpdateVolumeDiscountProgram':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewTransfer':
|
||||
case 'CancelTransfer':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_transfer_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredParticipation
|
||||
),
|
||||
};
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
const result: Record<
|
||||
ProposalChangeType,
|
||||
{
|
||||
requiredMajority: BigNumber;
|
||||
requiredParticipation: BigNumber;
|
||||
requiredMajorityLP: BigNumber;
|
||||
requiredParticipationLP: BigNumber;
|
||||
}
|
||||
> = {
|
||||
NewMarket: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_market_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_market_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
NewSpotMarket: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_market_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_market_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateMarket: {
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajority || 1
|
||||
),
|
||||
requiredMajorityLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP || 0
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation || 1
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP || 0
|
||||
),
|
||||
},
|
||||
UpdateMarketState: {
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajority || 1
|
||||
),
|
||||
requiredMajorityLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP || 0
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation || 1
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP || 0
|
||||
),
|
||||
},
|
||||
UpdateSpotMarket: {
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajority || 1
|
||||
),
|
||||
requiredMajorityLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP || 0
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation || 1
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP || 0
|
||||
),
|
||||
},
|
||||
UpdateNetworkParameter: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateNetParam_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateNetParam_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
NewAsset: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_asset_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_asset_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateAsset: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateAsset_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateAsset_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
NewFreeform: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_freeform_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_freeform_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateReferralProgram: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_referralProgram_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_referralProgram_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateVolumeDiscountProgram: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredParticipation ||
|
||||
1
|
||||
),
|
||||
},
|
||||
NewTransfer: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
CancelTransfer: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -79,33 +79,35 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.requiredMajorityPercentage).toEqual(new BigNumber(50));
|
||||
expect(current.requiredMajorityLPPercentage).toEqual(new BigNumber(50));
|
||||
expect(current.noTokens).toEqual(new BigNumber(60));
|
||||
expect(current.noVotes).toEqual(new BigNumber(60));
|
||||
expect(current.noEquityLikeShareWeight).toEqual(new BigNumber(70));
|
||||
expect(current.yesTokens).toEqual(new BigNumber(40));
|
||||
expect(current.yesVotes).toEqual(new BigNumber(40));
|
||||
expect(current.yesEquityLikeShareWeight).toEqual(new BigNumber(30));
|
||||
expect(current.totalTokensVoted).toEqual(new BigNumber(100));
|
||||
expect(current.totalVotes).toEqual(new BigNumber(100));
|
||||
expect(current.totalEquityLikeShareWeight).toEqual(new BigNumber(100));
|
||||
expect(current.yesPercentage).toEqual(new BigNumber(40));
|
||||
expect(current.yesLPPercentage).toEqual(new BigNumber(30));
|
||||
expect(current.noPercentage).toEqual(new BigNumber(60));
|
||||
expect(current.noLPPercentage).toEqual(new BigNumber(70));
|
||||
expect(current.requiredParticipation).toEqual(new BigNumber(50));
|
||||
expect(current.participationMet).toEqual(true);
|
||||
expect(current.requiredParticipationLP).toEqual(new BigNumber(50));
|
||||
expect(current.participationLPMet).toEqual(true);
|
||||
expect(current.majorityMet).toEqual(false);
|
||||
expect(current.majorityLPMet).toEqual(false);
|
||||
expect(current.totalTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current.totalLPTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current.willPassByLPVote).toEqual(false);
|
||||
expect(current?.requiredMajorityLPPercentage).toEqual(new BigNumber(50));
|
||||
expect(current?.requiredMajorityPercentage).toEqual(new BigNumber(50));
|
||||
expect(current?.noTokens).toEqual(new BigNumber(60));
|
||||
expect(current?.noVotes).toEqual(new BigNumber(60));
|
||||
expect(current?.noEquityLikeShareWeight).toEqual(new BigNumber(70));
|
||||
expect(current?.yesTokens).toEqual(new BigNumber(40));
|
||||
expect(current?.yesVotes).toEqual(new BigNumber(40));
|
||||
expect(current?.yesEquityLikeShareWeight).toEqual(new BigNumber(30));
|
||||
expect(current?.totalTokensVoted).toEqual(new BigNumber(100));
|
||||
expect(current?.totalVotes).toEqual(new BigNumber(100));
|
||||
expect(current?.totalEquityLikeShareWeight).toEqual(new BigNumber(100));
|
||||
expect(current?.yesPercentage).toEqual(new BigNumber(40));
|
||||
expect(current?.yesLPPercentage).toEqual(new BigNumber(30));
|
||||
expect(current?.noPercentage).toEqual(new BigNumber(60));
|
||||
expect(current?.noLPPercentage).toEqual(new BigNumber(70));
|
||||
expect(current?.requiredParticipation).toEqual(new BigNumber(50));
|
||||
expect(current?.participationMet).toEqual(true);
|
||||
expect(current?.requiredParticipationLP).toEqual(new BigNumber(50));
|
||||
expect(current?.participationLPMet).toEqual(true);
|
||||
expect(current?.majorityMet).toEqual(false);
|
||||
expect(current?.majorityLPMet).toEqual(false);
|
||||
expect(current?.totalTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current?.totalLPTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly returns majority, participation and will-pass status for a proposal with no votes', () => {
|
||||
@@ -123,11 +125,13 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(false);
|
||||
expect(current.majorityMet).toEqual(false);
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.participationMet).toEqual(false);
|
||||
expect(current?.majorityMet).toEqual(false);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows lack of participation for a failing proposal lacking votes', () => {
|
||||
@@ -145,9 +149,11 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(false);
|
||||
expect(current?.participationMet).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows participation but lack of majority for a failing proposal with enough votes but not enough majority', () => {
|
||||
@@ -165,11 +171,13 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(true);
|
||||
expect(current.majorityMet).toEqual(false);
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.participationMet).toEqual(true);
|
||||
expect(current?.majorityMet).toEqual(false);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows participation, majority and will-pass data for successful proposal', () => {
|
||||
@@ -187,11 +195,13 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(true);
|
||||
expect(current.majorityMet).toEqual(true);
|
||||
expect(current.willPassByTokenVote).toEqual(true);
|
||||
expect(current?.participationMet).toEqual(true);
|
||||
expect(current?.majorityMet).toEqual(true);
|
||||
expect(current?.willPassByTokenVote).toEqual(true);
|
||||
});
|
||||
|
||||
it('correctly shows whether an update market proposal will pass by token or LP vote - both failing', () => {
|
||||
@@ -221,10 +231,12 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current.willPassByLPVote).toEqual(false);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows whether an update market proposal failing token but passing LP voting', () => {
|
||||
@@ -254,9 +266,81 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current.willPassByLPVote).toEqual(true);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(true);
|
||||
});
|
||||
|
||||
it('mainnet recreation: only yes LP votes equal passing', () => {
|
||||
const yesVotes = 0;
|
||||
const noVotes = 70;
|
||||
const yesEquityLikeShareWeight = '0.21';
|
||||
const noEquityLikeShareWeight = '0';
|
||||
const fixedTokenValue = 1000000000000000000;
|
||||
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
marketId: '12345',
|
||||
},
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(
|
||||
yesVotes,
|
||||
fixedTokenValue,
|
||||
yesEquityLikeShareWeight
|
||||
),
|
||||
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(true);
|
||||
});
|
||||
|
||||
it('mainnet recreation: mixed yes and no LP votes equal failing', () => {
|
||||
const yesVotes = 0;
|
||||
const noVotes = 70;
|
||||
const yesEquityLikeShareWeight = '0.21';
|
||||
const noEquityLikeShareWeight = '0.22';
|
||||
const fixedTokenValue = 1000000000000000000;
|
||||
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
marketId: '12345',
|
||||
},
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(
|
||||
yesVotes,
|
||||
fixedTokenValue,
|
||||
yesEquityLikeShareWeight
|
||||
),
|
||||
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user