Compare commits

..
Author SHA1 Message Date
Matthew Russell 02597faeba feat: use new limit param for number formatting 2024-01-13 18:45:49 -05:00
Matthew Russell b216312a3e feat: update formatNumberRounded to accept a limit 2024-01-13 18:45:49 -05:00
Matthew Russell ab04c68e1b feat: add formatting for stat values 2024-01-13 18:45:49 -05:00
Matthew Russell 6049cb1dc7 chore: use built in team creation methods 2024-01-13 18:45:49 -05:00
Matthew Russell 9a04045d32 feat: responsive adjustments 2024-01-13 18:45:49 -05:00
Matthew Russell fa99dfde79 fix: missing i18n 2024-01-13 18:45:49 -05:00
Matthew Russell 8b826bce45 fix: use correct bg color for gradient 2024-01-13 18:45:49 -05:00
Matthew Russell 0f5d28c32a fix: incorrect member count 2024-01-13 18:45:49 -05:00
Matthew Russell 204adb4910 feat: add i18n 2024-01-13 18:45:49 -05:00
Matthew Russell 38edc24e02 feat: add favorite game and last 5 logic 2024-01-13 18:45:49 -05:00
Matthew Russell c476706cdf feat: add games list data 2024-01-13 18:45:48 -05:00
Matthew Russell f523366a64 feat: wire up games played 2024-01-13 18:44:55 -05:00
Matthew Russell 07285973d1 feat: wire up data, remove team pnl, improve key creation 2024-01-13 18:44:55 -05:00
Matthew Russell 72611fbb31 feat: add members table 2024-01-13 18:44:55 -05:00
Matthew Russell dc99dd592d feat: add members table 2024-01-13 18:44:55 -05:00
Matthew Russell be5d98c011 test: add team creation in sim 2024-01-13 18:44:55 -05:00
Matthew Russell 88e45271b1 feat: wire up avatar 2024-01-13 18:44:55 -05:00
Matthew Russell bec78a4c1a feat: add teams query 2024-01-13 18:44:54 -05:00
Matthew Russell b89b901f71 feat: richer cell content 2024-01-13 18:43:45 -05:00
Matthew Russell a5eb460d98 feat: add trading table component 2024-01-13 18:43:45 -05:00
Matthew Russell da43624478 feat: add favorite game and fix bg cover 2024-01-13 18:43:45 -05:00
Matthew Russell 6e6207e7f9 feat: update path to be nested under competitions 2024-01-13 18:43:45 -05:00
Matthew Russell 3957c2d911 feat: add cover bg and tooltips for stats 2024-01-13 18:43:45 -05:00
Matthew Russell 11010525df feat: add joined state 2024-01-13 18:43:45 -05:00
Matthew Russell c1d3d83045 feat: add basic table 2024-01-13 18:43:45 -05:00
Matthew Russell 9c7adb812c feat: add route and initial page layout 2024-01-13 18:43:45 -05:00
Matthew Russell 96cbbfc3a0 chore: tidy handling of proposal union type 2024-01-13 18:38:01 -05:00
Matthew Russell cb80d6c20b chore: handle proposal union type 2024-01-13 17:43:14 -05:00
Matthew Russell b74a04ed94 chore: fix ts errors in trading app 2024-01-13 16:23:33 -05:00
Matthew Russell fca8c19898 chore: fix ts errors in candles-chart 2024-01-13 16:21:57 -05:00
Matthew Russell cfb7715124 chore: fix type errors in market lib 2024-01-13 15:41:14 -05:00
Matthew Russell 1004347b33 chore: fix type errors in proposals lib 2024-01-13 15:40:58 -05:00
Matthew Russell d14469f2f9 chore: fix global type mappings 2024-01-12 18:50:37 -05:00
Matthew Russell da7dc1309b chore: fix type issues 2024-01-12 18:42:46 -05:00
Matthew Russell e57bf9a207 chore: use extract to narrow type 2024-01-12 18:21:22 -05:00
Matthew Russell 44842169e7 chore: regen types 2024-01-12 10:42:09 -05:00
236 changed files with 4832 additions and 23362 deletions
+3 -46
View File
@@ -19,7 +19,7 @@ jobs:
create-docker-image:
name: Create docker image for console-test
runs-on: ubuntu-22.04
timeout-minutes: 45
timeout-minutes: 20
steps:
#----------------------------------------------
# check-out frontend-monorepo
@@ -138,7 +138,7 @@ jobs:
name: run-tests
runs-on: 8-cores
needs: [create-docker-image, console-test-branch]
timeout-minutes: 45
timeout-minutes: 20
steps:
#----------------------------------------------
# load docker image
@@ -205,7 +205,7 @@ jobs:
# run tests
#----------------------------------------------
- name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 1 --dist loadfile --durations=45
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 4 --dist loadfile --durations=15
working-directory: apps/trading/e2e
#----------------------------------------------
# upload traces
@@ -227,46 +227,3 @@ jobs:
name: worker-logs
path: ./logs/
retention-days: 15
#----------------------------------------------
# ----- upload market-sim logs -----
#----------------------------------------------
- name: Prepare and Zip market-sim-logs
if: always()
run: |
parent_dir="/tmp/market-sim-logs"
echo "Creating parent directory at $parent_dir"
mkdir -p "$parent_dir"
echo "Waiting for vega-sim-* folders to be created..."
sleep 10 # Waits 10 seconds to ensure all folders are created
echo "Before searching for vega-sim-* folders in /tmp..."
folders=$(find /tmp -mindepth 1 -type d -name 'vega-sim-*' -print) || echo "Find command failed with exit code $?"
echo "After searching for vega-sim-* folders in /tmp..."
if [ -z "$folders" ]; then
echo "No vega-sim-* folders found."
exit 0
fi
echo "Moving vega-sim-* folders to $parent_dir"
echo "$folders" | xargs -I {} mv {} "$parent_dir/"
echo "Checking if $parent_dir is not empty..."
if [ "$(ls -A $parent_dir)" ]; then
echo "Zipping the parent directory..."
zip -r market-sim-logs.zip "$parent_dir" && echo "Zip file created successfully."
else
echo "$parent_dir is empty. No zip file created."
exit 0
fi
shell: /usr/bin/bash -e {0}
- name: Upload market-sim-logs
uses: actions/upload-artifact@v3
if: always()
with:
name: market-sim-logs
path: market-sim-logs.zip
retention-days: 15
+5 -5
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev).
## 🔎 Applications in this repo
# 🔎 Applications in this repo
### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
## 🧱 Libraries in this repo
# 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities.
## 💻 Develop
# 💻 Develop
### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
## 🐋 Hosting a console
# 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
```
## 📑 License
# 📑 License
[MIT](./LICENSE)
@@ -44,7 +44,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.getByTestId('icon-cross').click();
});
it('Proposal page displayed on mobile', function () {
it.skip('Proposal page displayed on mobile', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.common_switch_to_mobile_and_click_toggle();
@@ -55,7 +55,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
});
it.skip('Able to view new asset proposal', function () {
it('Able to view new asset proposal', function () {
const proposalTitle = 'Test new asset proposal';
const newAssetProposalBody = getNewAssetTxBody();
cy.VegaWalletSubmitProposal(newAssetProposalBody);
@@ -1,9 +1,11 @@
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
id
rationale {
title
description
... on Proposal {
id
rationale {
title
description
}
}
}
}
@@ -8,16 +8,18 @@ export type ExplorerProposalQueryVariables = Types.Exact<{
}>;
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export const ExplorerProposalDocument = gql`
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
id
rationale {
title
description
... on Proposal {
id
rationale {
title
description
}
}
}
}
@@ -1,7 +1,11 @@
import { useExplorerProposalQuery } from './__generated__/Proposal';
import {
useExplorerProposalQuery,
type ExplorerProposalQuery,
} from './__generated__/Proposal';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ENV } from '../../../config/env';
import Hash from '../hash';
export type ProposalLinkProps = {
id: string;
text?: string;
@@ -16,8 +20,13 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
variables: { id },
});
const proposal = data?.proposal as Extract<
ExplorerProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const base = ENV.dataSources.governanceUrl;
const label = data?.proposal?.rationale.title || id;
const label = proposal?.rationale.title || id;
return (
<ExternalLink href={`${base}/proposals/${id}`}>
@@ -1,5 +1,9 @@
import { render, waitFor } from '@testing-library/react';
import { NestedDataList, sortNestedDataByChildren } from './nested-data-list';
import {
BORDER_COLOURS,
NestedDataList,
sortNestedDataByChildren,
} from './nested-data-list';
import userEvent from '@testing-library/user-event';
const mockData = {
@@ -57,6 +61,38 @@ describe('NestedDataList', () => {
expect(parent[0].querySelector('li')).toHaveClass('pl-4 border-l-4 pt-2');
});
it('should repeat the border colours in the correct order', () => {
const colourMockData = {
t0: {
t1: {
t2: {
t3: {
t4: {
t5: {
t6: {
t7: {
t8: {
hello: 'world',
},
},
},
},
},
},
},
},
},
};
const tree = render(<NestedDataList data={colourMockData} />);
const { getByTestId } = tree;
for (let i = 0; i < 8; i++) {
const item = getByTestId(`T${i}`);
const expected = BORDER_COLOURS.light[i % 5];
expect(item.style.borderColor.toUpperCase()).toBe(expected);
}
});
it('should sort the data by values with children', () => {
const mockData = {
nonce: '5980890939790185837',
@@ -1,7 +1,9 @@
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
... on Proposal {
id
state
rejectionReason
}
}
}
@@ -8,15 +8,17 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
}>;
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql`
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
... on Proposal {
id
state
rejectionReason
}
}
}
`;
@@ -14,16 +14,18 @@ export function format(date: string | undefined, def: string) {
return new Date().toLocaleDateString() || def;
}
export function getDate(
data: ExplorerProposalStatusQuery | undefined,
terms: Terms
): string {
type Proposal = Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
export function getDate(proposal: Proposal | undefined, terms: Terms): string {
const DEFAULT = t('Unknown');
if (!data?.proposal?.state) {
if (!proposal?.state) {
return DEFAULT;
}
switch (data.proposal.state) {
switch (proposal.state) {
case 'STATE_DECLINED':
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_ENACTED':
@@ -62,9 +64,11 @@ export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
},
});
const proposal = data?.proposal as Proposal;
return (
<Lozenge className="font-sans text-xs float-right">
{getDate(data, terms)}
{getDate(proposal, terms)}
</Lozenge>
);
};
@@ -2,17 +2,8 @@ import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import type * as Apollo from '@apollo/client';
import type * as Types from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
type ProposalQueryResult = Apollo.QueryResult<
ExplorerProposalStatusQuery,
Types.Exact<{
id: string;
}>
>;
interface ProposalStatusIconProps {
id: string;
}
@@ -29,29 +20,38 @@ type IconAndLabel = {
* @param data a data result from useExplorerProposalStatusQuery
* @returns Icon name
*/
export function getIconAndLabelForStatus(
res: ProposalQueryResult
): IconAndLabel {
export function useIconAndLabelForStatus(id: string): IconAndLabel {
const { data, loading, error } = useExplorerProposalStatusQuery({
variables: {
id,
},
});
const proposal = data?.proposal as Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const DEFAULT: IconAndLabel = {
icon: 'error',
label: t('Proposal state unknown'),
};
if (res.loading) {
if (loading) {
return {
icon: 'more',
label: t('Loading data'),
};
}
if (!res?.data?.proposal || res.error) {
if (!data?.proposal || error) {
return {
icon: 'error',
label: res.error?.message || DEFAULT.label,
label: error?.message || DEFAULT.label,
};
}
switch (res.data.proposal.state) {
switch (proposal.state) {
case 'STATE_DECLINED':
return {
icon: 'stop',
@@ -99,13 +99,7 @@ export function getIconAndLabelForStatus(
/**
*/
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
const { icon, label } = getIconAndLabelForStatus(
useExplorerProposalStatusQuery({
variables: {
id,
},
})
);
const { icon, label } = useIconAndLabelForStatus(id);
return (
<div className="float-left mr-3">
@@ -1,54 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerReferralCodeOwnerQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerReferralCodeOwnerQuery = { __typename?: 'Query', referralSets: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', createdAt: any, updatedAt: any, referrer: string } } | null> } };
export const ExplorerReferralCodeOwnerDocument = gql`
query ExplorerReferralCodeOwner($id: ID!) {
referralSets(id: $id) {
edges {
node {
createdAt
updatedAt
referrer
}
}
}
}
`;
/**
* __useExplorerReferralCodeOwnerQuery__
*
* To run a query within a React component, call `useExplorerReferralCodeOwnerQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerReferralCodeOwnerQuery` 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 } = useExplorerReferralCodeOwnerQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerReferralCodeOwnerQuery(baseOptions: Apollo.QueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
}
export function useExplorerReferralCodeOwnerLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
}
export type ExplorerReferralCodeOwnerQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerQuery>;
export type ExplorerReferralCodeOwnerLazyQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerLazyQuery>;
export type ExplorerReferralCodeOwnerQueryResult = Apollo.QueryResult<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>;
@@ -1,11 +0,0 @@
query ExplorerReferralCodeOwner($id: ID!) {
referralSets(id: $id) {
edges {
node {
createdAt
updatedAt
referrer
}
}
}
}
@@ -1,95 +0,0 @@
import { render, waitFor } from '@testing-library/react';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { ReferralCodeOwner } from './referral-code-owner';
import type { ReferralCodeOwnerProps } from './referral-code-owner';
import { ExplorerReferralCodeOwnerDocument } from './__generated__/code-owner';
const renderComponent = (
props: ReferralCodeOwnerProps,
mocks: MockedResponse[]
) => {
return render(
<MockedProvider mocks={mocks}>
<MemoryRouter>
<ReferralCodeOwner {...props} />
</MemoryRouter>
</MockedProvider>
);
};
describe('ReferralCodeOwner', () => {
it('should render loading state', () => {
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
expect(getByText('Loading...')).toBeInTheDocument();
});
it('should render error state', async () => {
const errorMessage = 'Error fetching referrer: ABC123';
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
error: new Error('nope'),
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
await waitFor(() => {
expect(getByText(errorMessage)).toBeInTheDocument();
});
});
it('should render link to referring party', async () => {
const referrerId = 'DEF789';
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
result: {
data: {
referralSets: {
edges: [
{
node: {
__typename: 'ReferralSet',
referrer: referrerId,
createdAt: '123',
updatedAt: '456',
},
},
],
},
},
},
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
await waitFor(() => {
expect(getByText(referrerId)).toBeInTheDocument();
});
});
});
@@ -1,26 +0,0 @@
import { TableCell } from '../../../table';
import { useExplorerReferralCodeOwnerQuery } from './__generated__/code-owner';
import { PartyLink } from '../../../links';
export interface ReferralCodeOwnerProps {
code: string;
}
/**
* Render the owner of a referral code
*/
export const ReferralCodeOwner = ({ code }: ReferralCodeOwnerProps) => {
const { data, error, loading } = useExplorerReferralCodeOwnerQuery({
variables: {
id: code,
},
});
const referrer = data?.referralSets.edges[0]?.node.referrer || '';
return (
<TableCell>
{loading && 'Loading...'}
{error && `Error fetching referrer: ${code}`}
{referrer.length > 0 && <PartyLink id={referrer} />}
</TableCell>
);
};
@@ -1,93 +0,0 @@
import { render } from '@testing-library/react';
import { ReferralTeam } from './team';
import type { CreateReferralSet } from './team';
import { MockedProvider } from '@apollo/client/testing';
describe('ReferralTeam', () => {
const team = {
name: 'Test Team',
teamUrl: 'https://example.com/team',
avatarUrl: 'https://example.com/avatar',
closed: false,
};
const mockTx: CreateReferralSet = {
team,
};
const mockId = '123456';
const mockCreator = 'JohnDoe';
it('should render the team name', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Test Team')).toBeInTheDocument();
});
it('should render the team ID', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Id')).toBeInTheDocument();
expect(getByText(mockId)).toBeInTheDocument();
});
it('should render the creator', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Creator')).toBeInTheDocument();
expect(getByText(mockCreator)).toBeInTheDocument();
});
it('should render the team URL', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Team URL')).toBeInTheDocument();
expect(getByText(team.teamUrl)).toBeInTheDocument();
});
it('should render the avatar URL', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Avatar')).toBeInTheDocument();
expect(getByText(team.avatarUrl)).toBeInTheDocument();
});
it('should render the open status as a tick if closed is falsy', () => {
const { getByTestId } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByTestId('open-yes')).toBeInTheDocument();
});
it('should render the open status as a cross if it is truthy', () => {
const m = {
team: {
closed: true,
},
};
const { getByTestId } = render(
<MockedProvider>
<ReferralTeam tx={m} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByTestId('open-no')).toBeInTheDocument();
});
});
@@ -1,73 +0,0 @@
import {
VegaIcon,
Icon,
KeyValueTable,
KeyValueTableRow,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
import Hash from '../../../links/hash';
import { t } from 'i18next';
import { PartyLink } from '../../../links';
export type CreateReferralSet = components['schemas']['v1CreateReferralSet'];
export type ReferralTeam = CreateReferralSet['team'];
export interface ReferralTeamProps {
tx: CreateReferralSet;
id: string;
creator: string;
}
/**
* Renders the details for a team in a CreateReferralSet or UpdateReferralSet transaction.
*
* Intentionally does not render the avatar image or link to the team url.
*/
export const ReferralTeam = ({ tx, id, creator }: ReferralTeamProps) => {
if (!tx.team) {
return null;
}
return (
<section>
<div className="inline-block mr-2 leading-none">
<VegaIcon name={VegaIconNames.TEAM} />
</div>
{tx.team.name && (
<h3 className="inline-block leading-loose">{tx.team.name}</h3>
)}
<div className="min-w-fit max-w-2xl block">
<KeyValueTable>
<KeyValueTableRow>
{t('Id')}
<Hash text={id} truncate={false} />
</KeyValueTableRow>
<KeyValueTableRow>
{t('Creator')}
<PartyLink id={creator} truncate={false} />
</KeyValueTableRow>
{tx.team.teamUrl && (
<KeyValueTableRow>
{t('Team URL')}
<Hash text={tx.team.teamUrl} truncate={false} />
</KeyValueTableRow>
)}
{tx.team.avatarUrl && (
<KeyValueTableRow>
{t('Avatar')}
<Hash text={tx.team.avatarUrl} truncate={false} />
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('Open')}
<span data-testid={!tx.team.closed ? 'open-yes' : 'open-no'}>
{!tx.team.closed ? <Icon name="tick" /> : <Icon name="cross" />}
</span>
</KeyValueTableRow>
</KeyValueTable>
</div>
</section>
);
};
@@ -27,8 +27,7 @@ export const sharedHeaderProps = {
className: 'align-top',
};
// The incoming type field is usually the right thing to show. Exceptions are listed here
const LabelOverrides: Record<BlockExplorerTransactionResult['type'], string> = {
const Labels: Record<BlockExplorerTransactionResult['type'], string> = {
'Stop Orders Submission': 'Stop Order',
'Stop Orders Cancellation': 'Cancel Stop Order',
};
@@ -51,7 +50,7 @@ export const TxDetailsShared = ({
const time: string = blockData?.result.block.header.time || '';
const height: string = blockData?.result.block.header.height || txData.block;
const type = LabelOverrides[txData.type] || txData.type;
const type = Labels[txData.type] || txData.type;
return (
<>
@@ -1,47 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { ReferralCodeOwner } from './referrals/referral-code-owner';
interface TxDetailsApplyReferralCodeProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* The signature can be turned in to an id with txSignatureToDeterministicId but
*/
export const TxDetailsApplyReferralCode = ({
txData,
pubKey,
blockData,
}: TxDetailsApplyReferralCodeProps) => {
if (!txData || !txData.command.applyReferralCode || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const referralCode = txData.command.applyReferralCode.id;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Applied Code')}</TableCell>
<TableCell>{referralCode}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Referrer')}</TableCell>
<ReferralCodeOwner code={referralCode} />
</TableRow>
</TableWithTbody>
</div>
);
};
@@ -1,52 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import { ReferralTeam } from './referrals/team';
interface TxDetailsCreateReferralProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* The signature can be turned in to an id with txSignatureToDeterministicId but
*/
export const TxDetailsCreateReferralSet = ({
txData,
pubKey,
blockData,
}: TxDetailsCreateReferralProps) => {
if (!txData || !txData.command.createReferralSet || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const id = txSignatureToDeterministicId(txData.signature.value);
const isTeam = txData.command.createReferralSet.isTeam || false;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{isTeam ? t('Team ID') : t('Referral code')}</TableCell>
<TableCell>{id}</TableCell>
</TableRow>
</TableWithTbody>
<ReferralTeam
tx={txData.command.createReferralSet}
id={id}
creator={txData.submitter}
/>
</div>
);
};
@@ -28,10 +28,6 @@ import { TxProposal } from './tx-proposal';
import { TxDetailsTransfer } from './tx-transfer';
import { TxDetailsStopOrderSubmission } from './tx-stop-order-submission';
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
import { TxDetailsCreateReferralSet } from './tx-create-referral-set';
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -125,14 +121,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsStopOrderSubmission;
case 'Transfer Funds':
return TxDetailsTransfer;
case 'Create Referral Set':
return TxDetailsCreateReferralSet;
case 'Update Referral Set':
return TxDetailsUpdateReferralSet;
case 'Apply Referral Code':
return TxDetailsApplyReferralCode;
case 'Join Team':
return TxDetailsJoinTeam;
default:
return TxDetailsGeneric;
}
@@ -1,47 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { ReferralCodeOwner } from './referrals/referral-code-owner';
interface TxDetailsJoinTeamProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* The signature can be turned in to an id with txSignatureToDeterministicId but
*/
export const TxDetailsJoinTeam = ({
txData,
pubKey,
blockData,
}: TxDetailsJoinTeamProps) => {
if (!txData || !txData.command.joinTeam || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const team = txData.command.joinTeam.id;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Team')}</TableCell>
<TableCell>{team}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Referrer')}</TableCell>
<ReferralCodeOwner code={team} />
</TableRow>
</TableWithTbody>
</div>
);
};
@@ -1,54 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import { ReferralTeam } from './referrals/team';
interface TxDetailsUpdateReferralProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* A copy of create referral set, effectively.
* Updating a referral set without a team doesn't make sense,
* but is valid, so this component ignores sense.
*/
export const TxDetailsUpdateReferralSet = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdateReferralProps) => {
if (!txData || !txData.command.updateReferralSet || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const id = txSignatureToDeterministicId(txData.signature.value);
const isTeam = txData.command.updateReferralSet.isTeam || false;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{isTeam ? t('Team ID') : t('Referral code')}</TableCell>
<TableCell>{id}</TableCell>
</TableRow>
</TableWithTbody>
<ReferralTeam
tx={txData.command.updateReferralSet}
id={id}
creator={txData.submitter}
/>
</div>
);
};
@@ -28,7 +28,6 @@ export type FilterOption =
| 'Delegate'
| 'Ethereum Key Rotate Submission'
| 'Issue Signatures'
| 'Join Team'
| 'Key Rotate Submission'
| 'Liquidity Provision Order'
| 'Node Signature'
@@ -48,46 +47,45 @@ export type FilterOption =
| 'Vote on Proposal'
| 'Withdraw';
export const filterOptions: Record<string, FilterOption[]> = {
'Market Instructions': [
'Amend LiquidityProvision Order',
'Amend Order',
'Batch Market Instructions',
'Cancel LiquidityProvision Order',
'Cancel Order',
'Liquidity Provision Order',
'Stop Orders Submission',
'Stop Orders Cancellation',
'Submit Order',
],
'Transfers and Withdrawals': [
'Transfer Funds',
'Cancel Transfer Funds',
'Withdraw',
],
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
Referrals: [
'Apply Referral Code',
'Create Referral Set',
'Join Team',
'Update Referral Set',
],
'External Data': ['Chain Event', 'Submit Oracle Data'],
Validators: [
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
],
};
// Alphabetised list of transaction types to appear at the top level
export const PrimaryFilterOptions: FilterOption[] = [
'Amend LiquidityProvision Order',
'Amend Order',
'Batch Market Instructions',
'Cancel LiquidityProvision Order',
'Cancel Order',
'Cancel Transfer Funds',
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Stop Orders Submission',
'Stop Orders Cancellation',
'Submit Oracle Data',
'Submit Order',
'Transfer Funds',
'Undelegate',
'Vote on Proposal',
'Withdraw',
];
export const AllFilterOptions: FilterOption[] =
Object.values(filterOptions).flat();
// Alphabetised list of transaction types to nest under a 'More...' submenu
export const SecondaryFilterOptions: FilterOption[] = [
'Chain Event',
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
];
export const AllFilterOptions: FilterOption[] = [
...PrimaryFilterOptions,
...SecondaryFilterOptions,
];
export interface TxFilterProps {
filters: Set<FilterOption>;
@@ -124,33 +122,46 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
<DropdownMenuSeparator />
</>
)}
{Object.entries(filterOptions).map(([key, value]) => (
<DropdownMenuSub key={key}>
<DropdownMenuSubTrigger>
{t(key)}
<Icon name="chevron-right" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{value.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={(checked) => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" className="inline" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
{PrimaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={() => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{t('More Types')}
<Icon name="chevron-right" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{SecondaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={(checked) => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" className="inline" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
);
@@ -48,8 +48,6 @@ const displayString: StringMap = {
StopOrdersSubmission: 'Stop',
StopOrdersCancellation: 'Cancel stop',
'Stop Orders Cancellation': 'Cancel stop',
'Apply Referral Code': 'Referral',
'Create Referral Set': 'Create referral',
};
export function getLabelForStopOrderType(
@@ -31,10 +31,6 @@ const Tx = () => {
}
}
if (!data || !data?.transaction) {
errorMessage = 'Transaction not found';
}
return (
<section>
<PageHeader
@@ -53,7 +49,7 @@ const Tx = () => {
<TxDetails
className="mb-28"
txData={data?.transaction}
pubKey={data?.transaction?.submitter}
pubKey={data?.transaction.submitter}
/>
</RenderFetched>
</section>
@@ -11,8 +11,8 @@ interface TxDetailsProps {
export const txDetailsTruncateLength = 30;
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
if (!txData || !pubKey) {
return <>{t('Transaction could not be found')}</>;
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
return (
<section className="mb-10" key={txData.hash}>
+2 -2
View File
@@ -8,13 +8,13 @@ type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = T | U extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-explicit-any */
type OneOf<T extends any[]> = T extends [infer Only]
? Only
: T extends [infer A, infer B, ...infer Rest]
? OneOf<[XOR<A, B>, ...Rest]>
: never;
/* eslint-enable @typescript-eslint/no-explicit-any */
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface paths {
'/info': {
@@ -47,7 +47,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
.and('contain.text', 'USDC (fake)');
});
it.skip('Unable to submit proposal with public key', function () {
it('Unable to submit proposal with public key', function () {
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
goToMakeNewProposal(governanceProposalType.RAW);
@@ -55,10 +55,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.getByTestId('dialog-content')
.first()
.within(() => {
cy.getByTestId('dialog-title').should(
'have.text',
'Transaction failed'
);
cy.get('h1').should('have.text', 'Transaction failed');
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
});
});
@@ -112,7 +112,6 @@ export function createNewMarketProposalTxBody(): ProposalSubmissionBody {
performanceHysteresisEpochs: 2,
slaCompetitionFactor: '0.1',
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
quadraticSlippageFactor: '0',
instrument: {
name: 'Token test market',
@@ -197,7 +196,6 @@ export function createNewMarketProposalTxBody(): ProposalSubmissionBody {
timeWindow: '3600',
scalingFactor: 10,
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
triggeringRatio: '0.7',
auctionExtension: '1',
},
@@ -242,7 +240,6 @@ export function createSuccessorMarketProposalTxBody(
decimalPlaces: '5',
positionDecimalPlaces: '5',
linearSlippageFactor: '0.001',
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
quadraticSlippageFactor: '0',
liquiditySlaParameters: {
priceRange: '0.5',
@@ -337,7 +334,6 @@ export function createSuccessorMarketProposalTxBody(
timeWindow: '3600',
scalingFactor: 10,
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
triggeringRatio: '0.7',
auctionExtension: '1',
},
+2 -9
View File
@@ -26,7 +26,7 @@ import {
} from '@vegaprotocol/web3';
import { Web3Provider } from '@vegaprotocol/web3';
import { VegaWalletDialogs } from './components/vega-wallet-dialogs';
import { VegaWalletProvider, useChainId } from '@vegaprotocol/wallet';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import {
useVegaTransactionManager,
useVegaTransactionUpdater,
@@ -96,9 +96,7 @@ const cache: InMemoryCacheConfig = {
const Web3Container = ({
chainId,
}: {
/** Ethereum chain id */
chainId: number;
/** Ethereum provider url */
providerUrl: string;
}) => {
const InitializeHandlers = () => {
@@ -125,9 +123,6 @@ const Web3Container = ({
MOZILLA_EXTENSION_URL,
VEGA_WALLET_URL,
} = useEnvironment();
const vegaChainId = useChainId(VEGA_URL);
useEffect(() => {
if (chainId) {
return initializeConnectors(
@@ -162,8 +157,7 @@ const Web3Container = ({
!VEGA_EXPLORER_URL ||
!DocsLinks ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL ||
!vegaChainId
!MOZILLA_EXTENSION_URL
) {
return null;
}
@@ -175,7 +169,6 @@ const Web3Container = ({
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
chainId: vegaChainId,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
-112
View File
@@ -1,112 +0,0 @@
import type { ApolloError } from '@apollo/client';
import {
PARTY_NOT_FOUND,
filterAcceptableGraphqlErrors,
isPartyNotFoundError,
} from './party';
import type { GraphQLError } from 'graphql';
/**
*
* @param message
* @returns GraphQLError
*/
function createMockApolloErrors(message: string): GraphQLError {
return {
message,
extensions: {
code: message.toUpperCase().replace(/ /g, '_'),
},
locations: [],
originalError: new Error(message),
path: [],
nodes: [],
positions: [1],
name: message,
source: {
body: message,
name: message,
locationOffset: {
line: 1,
column: 1,
},
},
};
}
describe('filterAcceptableGraphqlErrors', () => {
it('should return undefined if the error is a party not found error', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [createMockApolloErrors('failed to get party for ID')],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toBeUndefined();
});
it('should return the error if it is not a party not found error', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [createMockApolloErrors('Some other error')],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toEqual(error);
});
it('should return the error if there are multiple errors', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [
createMockApolloErrors('failed to get party for ID'),
createMockApolloErrors('Some other error'),
],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toEqual(error);
});
it('should return the error if there are no errors', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toEqual(error);
});
it('should return undefined if the error is undefined', () => {
const result = filterAcceptableGraphqlErrors(undefined);
expect(result).toBeUndefined();
});
});
describe('isPartyNotFoundError', () => {
it('should return true if the error message includes PARTY_NOT_FOUND', () => {
const error = { message: 'failed to get party for ID' };
const result = isPartyNotFoundError(error);
expect(result).toBe(true);
});
it('should return false if the error message does not include PARTY_NOT_FOUND', () => {
const error = { message: 'Some other error' };
const result = isPartyNotFoundError(error);
expect(result).toBe(false);
});
// Will trip if the error message changes, which should not be a problem, but there
// might be logic that depends on it
it('expects party not found error to remain consistent', () => {
const error = 'failed to get party for ID';
expect(PARTY_NOT_FOUND).toStrictEqual(error);
});
});
-22
View File
@@ -1,5 +1,3 @@
import type { ApolloError } from '@apollo/client';
export const PARTY_NOT_FOUND = 'failed to get party for ID';
export const isPartyNotFoundError = (error: { message: string }) => {
@@ -8,23 +6,3 @@ export const isPartyNotFoundError = (error: { message: string }) => {
}
return false;
};
/**
* If a party has no accounts or data, then this GraphQL query believes it does not exist
* Not having any rewards is a valid state, so in some cases we can filter this error out.
*
* @param error ApolloError | undefined
* @returns ApolloError | undefined
*/
export function filterAcceptableGraphqlErrors(
error?: ApolloError
): ApolloError | undefined {
// Currently the only error we expect is when a party has no accounts
if (error && error.graphQLErrors.length === 1) {
if (isPartyNotFoundError(error.graphQLErrors[0])) {
return;
}
}
return error;
}
+2 -2
View File
@@ -31,7 +31,7 @@ import {
orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list';
import { BigNumber } from '../../lib/bignumber';
import type { ProposalQuery } from '../proposals/proposal/__generated__/Proposal';
import { type Proposal } from '../proposals/types';
const nodesToShow = 6;
@@ -39,7 +39,7 @@ const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: ProposalQuery['proposal'][];
proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
@@ -1,16 +1,11 @@
import { useTranslation } from 'react-i18next';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
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 ReactNode } from 'react';
import { type ProposalInfoLabelVariant } from '../proposal-info-label';
import { type Proposal } from '../../types';
export const CurrentProposalState = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
const { t } = useTranslation();
let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant;
@@ -1,272 +0,0 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { CurrentProposalStatus } from './current-proposal-status';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
request: {
query: NetworkParamsDocument,
},
result: {
data: {
networkParametersConnection: {
edges: [
{
node: {
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.requiredMajority',
value: '0.00000001',
},
},
{
node: {
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.requiredParticipation',
value: '0.000000001',
},
},
],
},
},
},
};
const renderComponent = ({
proposal,
}: {
proposal: ProposalQuery['proposal'];
}) => {
render(
<AppStateProvider>
<MockedProvider mocks={[networkParamsQueryMock]}>
<CurrentProposalStatus proposal={proposal} />
</MockedProvider>
</AppStateProvider>
);
};
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(60 * 60 * 1000);
});
afterEach(() => {
jest.useRealTimers();
});
it('Proposal open - renders will fail state if the proposal will fail', async () => {
const failedProposal = generateProposal({
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal: failedProposal });
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('fail.')).toBeInTheDocument();
});
it('Proposal open - renders will pass state if the proposal will pass', async () => {
const proposal = generateProposal();
renderComponent({ proposal });
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('pass.')).toBeInTheDocument();
});
it('Proposal enacted - renders vote passed and time since enactment', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
enactmentDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal passed - renders vote passed and time since vote closed', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal waiting for node vote - will pass - renders if the vote will pass and status', async () => {
const failedProposal = generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal: failedProposal });
expect(
await screen.findByText('Waiting for nodes to validate asset.')
).toBeInTheDocument();
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('fail.')).toBeInTheDocument();
});
it('Proposal waiting for node vote - will fail - renders if the vote will pass and status', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
});
renderComponent({ proposal });
expect(
await screen.findByText('Waiting for nodes to validate asset.')
).toBeInTheDocument();
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('pass.')).toBeInTheDocument();
});
it('Proposal failed - renders vote failed reason and vote closed ago', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
errorDetails: 'foo',
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('foo')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders rejection reason there are no error details', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
rejectionReason: ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(
await screen.findByText('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE')
).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders unknown reason if there are no error details or rejection reason', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('unknown reason')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders participation not met if participation is not met', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('Participation not met')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders majority not met if majority is not met', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '1',
totalTokens: '25242474195500835440000',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('Majority not met')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
@@ -1,143 +0,0 @@
import type { ReactNode } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { ProposalState } from '@vegaprotocol/types';
import { useVoteInformation } from '../../hooks';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const StatusPass = ({ children }: { children: ReactNode }) => (
<span className="text-vega-green">{children}</span>
);
export const StatusFail = ({ children }: { children: ReactNode }) => (
<span className="text-danger">{children}</span>
);
const WillPass = ({
willPass,
children,
}: {
willPass: boolean;
children?: ReactNode;
}) => {
const { t } = useTranslation();
if (willPass) {
return (
<>
{children}
<StatusPass>{t('pass')}.</StatusPass>
<span className="ml-2">{t('finalOutcomeMayDiffer')}</span>
</>
);
} else {
return (
<>
{children}
<StatusFail>{t('fail')}.</StatusFail>
<span className="ml-2">{t('finalOutcomeMayDiffer')}</span>
</>
);
}
};
export const CurrentProposalStatus = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { willPassByTokenVote, majorityMet, participationMet } =
useVoteInformation({
proposal,
});
const { t } = useTranslation();
const daysClosedAgo = formatDistanceToNow(
new Date(proposal?.terms.closingDatetime),
{ addSuffix: true }
);
const daysEnactedAgo =
proposal?.terms.enactmentDatetime &&
formatDistanceToNow(new Date(proposal.terms.enactmentDatetime), {
addSuffix: true,
});
if (proposal?.state === ProposalState.STATE_OPEN) {
return (
<WillPass willPass={willPassByTokenVote}>{t('currentlySetTo')}</WillPass>
);
}
if (
proposal?.state === ProposalState.STATE_FAILED ||
proposal?.state === ProposalState.STATE_DECLINED ||
proposal?.state === ProposalState.STATE_REJECTED
) {
if (!participationMet) {
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>{t('participationNotMet')}</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
if (!majorityMet) {
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>{t('majorityNotMet')}</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>
{proposal?.errorDetails ||
proposal?.rejectionReason ||
t('unknownReason')}
</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
if (
proposal?.state === ProposalState.STATE_ENACTED ||
proposal?.state === ProposalState.STATE_PASSED
) {
return (
<>
<span>{t('votePassed')}</span>
<StatusPass>
&nbsp;
{proposal?.state === ProposalState.STATE_ENACTED
? t('Enacted')
: t('Passed')}
</StatusPass>
<span>
&nbsp;
{proposal?.state === ProposalState.STATE_ENACTED
? daysEnactedAgo
: daysClosedAgo}
</span>
</>
);
}
if (proposal?.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) {
return (
<WillPass willPass={willPassByTokenVote}>
<span>{t('WaitingForNodeVote')}</span>{' '}
<span>{t('currentlySetTo')}</span>
</WillPass>
);
}
return null;
};
@@ -1 +0,0 @@
export { CurrentProposalStatus } from './current-proposal-status';
@@ -6,11 +6,10 @@ import {
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
interface ProposalChangeTableProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
@@ -23,8 +23,8 @@ 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 type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { type MockedResponse } from '@apollo/client/testing';
import { type Proposal } from '../../types';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
@@ -36,7 +36,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
proposal: Proposal,
isListItem = true,
mocks: MockedResponse[] = [],
voteState?: VoteState
@@ -64,6 +64,7 @@ describe('Proposal header', () => {
it('Renders New market proposal', () => {
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New some market',
@@ -102,6 +103,7 @@ describe('Proposal header', () => {
it('Renders Update market proposal', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New market id',
@@ -124,12 +126,13 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description')
).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update to market ID: MarketId'
'Market change: MarketId'
);
});
it('Renders New asset proposal - ERC20', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New asset: Fake currency',
@@ -159,6 +162,7 @@ describe('Proposal header', () => {
it('Renders New asset proposal - BuiltInAsset', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -184,6 +188,7 @@ describe('Proposal header', () => {
it('Renders Update network', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'Network parameter',
@@ -213,6 +218,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - short rationale', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'short',
rationale: {
@@ -234,6 +240,7 @@ 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: {
@@ -259,6 +266,7 @@ 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: {
@@ -280,6 +288,7 @@ describe('Proposal header', () => {
it('Renders asset change proposal header', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -297,6 +306,7 @@ describe('Proposal header', () => {
it("Renders unknown proposal if it's a different proposal type", () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -313,6 +323,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Enacted', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
@@ -325,6 +336,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Passed', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
@@ -338,6 +350,7 @@ 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: {
@@ -352,6 +365,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Open', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_OPEN,
votes: {
@@ -1,15 +1,8 @@
import { useTranslation } from 'react-i18next';
import {
CopyWithTooltip,
Lozenge,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type ReactNode } from 'react';
import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
@@ -18,24 +11,20 @@ import {
useNewTransferProposalDetails,
useSuccessorMarketProposalDetails,
} from '@vegaprotocol/proposals';
import {
CONSOLE_MARKET_PAGE,
DApp,
useFeatureFlags,
useLinks,
} from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import Routes from '../../../routes';
import { Link } from 'react-router-dom';
import type { VoteState } from '../vote-details/use-user-vote';
import { type VoteState } from '../vote-details/use-user-vote';
import { VoteBreakdown } from '../vote-breakdown';
import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
import { type Proposal } from '../../types';
export const ProposalHeader = ({
proposal,
isListItem = true,
voteState,
}: {
proposal: ProposalQuery['proposal'];
proposal: Proposal;
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
@@ -43,8 +32,6 @@ export const ProposalHeader = ({
const { t } = useTranslation();
const change = proposal?.terms.change;
const consoleLink = useLinks(DApp.Console);
let details: ReactNode;
let proposalType = '';
let fallbackTitle = '';
@@ -53,7 +40,7 @@ export const ProposalHeader = ({
const titleContent = shorten(title ?? '', 100);
const getAsset = (proposal: ProposalQuery['proposal']) => {
const getAsset = (proposal: Proposal) => {
const terms = proposal?.terms;
if (
terms?.change.__typename === 'NewMarket' &&
@@ -119,33 +106,8 @@ export const ProposalHeader = ({
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>
<span>{t('MarketChange')}:</span>{' '}
<span>{truncateMiddle(change.marketId)}</span>
</>
);
break;
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -14,9 +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';
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
proposal: Proposal | null;
}
export const formatEndOfProgramTimestamp = (value: string) => {
@@ -1,6 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { useCancelTransferProposalDetails } from '@vegaprotocol/proposals';
import {
KeyValueTable,
@@ -8,11 +6,12 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
export const ProposalCancelTransferDetails = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { t } = useTranslation();
const details = useCancelTransferProposalDetails(proposal?.id);
@@ -1,6 +1,4 @@
import { useState } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { useTranslation } from 'react-i18next';
@@ -21,11 +19,12 @@ import {
addDecimalsFormatNumberQuantum,
formatDateWithLocalTimezone,
} from '@vegaprotocol/utils';
import { type Proposal } from '../../types';
export const ProposalTransferDetails = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { t } = useTranslation();
const [show, setShow] = useState(false);
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -12,6 +11,7 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
// 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: ProposalQuery['proposal'];
proposal: Proposal | null;
}
/**
@@ -5,13 +5,13 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { Row } from '@vegaprotocol/markets';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
interface ProposalUpdateMarketStateProps {
proposal: ProposalQuery['proposal'];
proposal: Proposal | null;
}
export const ProposalUpdateMarketState = ({
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -12,9 +11,10 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
proposal: Proposal | null;
}
export const formatVolumeDiscountFactor = (value: string) => {
@@ -1,13 +1,13 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import type { VegaWalletConfig } from '@vegaprotocol/wallet';
import { type VegaWalletConfig } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types';
import { mockNetworkParams } from '../../test-helpers/mocks';
import { type Proposal as IProposal } from '../../types';
jest.mock('@vegaprotocol/network-parameters', () => ({
...jest.requireActual('@vegaprotocol/network-parameters'),
@@ -48,17 +48,16 @@ const vegaWalletConfig: VegaWalletConfig = {
chromeExtensionUrl: 'chrome',
mozillaExtensionUrl: 'mozilla',
},
chainId: 'VEGA_CHAIN_ID',
};
const renderComponent = (proposal: ProposalQuery['proposal']) => {
const renderComponent = (proposal: IProposal) => {
render(
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<Proposal
restData={{}}
proposal={proposal as ProposalQuery['proposal']}
proposal={proposal}
networkParams={mockNetworkParams}
/>
</VegaWalletProvider>
@@ -12,14 +12,13 @@ import { UserVote } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfo } from '@vegaprotocol/markets';
import type { AssetQuery } from '@vegaprotocol/assets';
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 NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote';
import {
@@ -28,9 +27,10 @@ import {
} from '../proposal-transfer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { type Proposal as IProposal } from '../../types';
export interface ProposalProps {
proposal: ProposalQuery['proposal'];
proposal: IProposal;
networkParams: Partial<NetworkParamsResult>;
marketData?: MarketInfo | null;
parentMarketData?: MarketInfo | null;
@@ -1,7 +1,7 @@
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 { type MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { format } from 'date-fns';
@@ -18,10 +18,10 @@ import {
lastWeek,
nextWeek,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
const renderComponent = (
proposal: ProposalQuery['proposal'],
proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
@@ -1,21 +1,20 @@
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 type { ReactNode } from 'react';
import {
ProposalRejectionReasonMapping,
ProposalState,
} from '@vegaprotocol/types';
import Routes from '../../../routes';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
export const ProposalsListItemDetails = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { t } = useTranslation();
const state = proposal?.state;
@@ -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 { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
interface ProposalsListItemProps {
proposal?: ProposalQuery['proposal'] | null;
proposal?: Proposal | null;
}
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
@@ -17,8 +17,8 @@ import {
lastMonth,
nextMonth,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types';
const openProposalClosesNextMonth = generateProposal({
id: 'proposal1',
@@ -63,7 +63,7 @@ const closedProtocolUpgradeProposal = generateProtocolUpgradeProposal({
});
const renderComponent = (
proposals: ProposalQuery['proposal'][],
proposals: Proposal,
protocolUpgradeProposals?: ProtocolUpgradeProposalFieldsFragment[]
) => (
<Router>
@@ -10,20 +10,20 @@ import Routes from '../../../routes';
import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ExternalLinks } from '@vegaprotocol/environment';
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types';
interface ProposalsListProps {
proposals: Array<ProposalQuery['proposal']>;
proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string;
}
interface SortedProposalsProps {
open: ProposalQuery['proposal'][];
closed: ProposalQuery['proposal'][];
open: Proposal[];
closed: Proposal[];
}
interface SortedProtocolUpgradeProposalsProps {
@@ -31,7 +31,7 @@ interface SortedProtocolUpgradeProposalsProps {
closed: ProtocolUpgradeProposalFieldsFragment[];
}
export const orderByDate = (arr: ProposalQuery['proposal'][]) =>
export const orderByDate = (arr: Proposal[]) =>
orderBy(
arr,
[
@@ -91,14 +91,10 @@ export const ProposalsList = ({
);
return {
open:
initialSorting.open.length > 0
? orderByDate(initialSorting.open as ProposalQuery['proposal'][])
: [],
initialSorting.open.length > 0 ? orderByDate(initialSorting.open) : [],
closed:
initialSorting.closed.length > 0
? orderByDate(
initialSorting.closed as ProposalQuery['proposal'][]
).reverse()
? orderByDate(initialSorting.closed).reverse()
: [],
};
}, [proposals]);
@@ -125,9 +121,7 @@ export const ProposalsList = ({
};
}, [protocolUpgradeProposals, lastBlockHeight]);
const filterPredicate = (
p: ProposalFieldsFragment | ProposalQuery['proposal']
) =>
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -12,7 +12,7 @@ import {
nextWeek,
lastMonth,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
const rejectedProposalClosesNextWeek = generateProposal({
id: 'rejected1',
@@ -35,7 +35,7 @@ const rejectedProposalClosedLastMonth = generateProposal({
},
});
const renderComponent = (proposals: ProposalQuery['proposal'][]) => (
const renderComponent = (proposals: Proposal[]) => (
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
@@ -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 { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
interface ProposalsListProps {
proposals: ProposalQuery['proposal'][];
proposals: Proposal[];
}
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const filterPredicate = (p: ProposalQuery['proposal']) =>
const filterPredicate = (p: Proposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -9,8 +9,7 @@ import {
nextWeek,
} from '../../test-helpers/mocks';
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { type MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
@@ -18,7 +17,8 @@ import {
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
import { type AppState } from '../../../../contexts/app-state/app-state-context';
import { type Proposal } from '../../types';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
@@ -41,7 +41,7 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
@@ -1,3 +1,4 @@
import { type ReactNode } from 'react';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
@@ -5,10 +6,8 @@ import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { type Proposal } from '../../types';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
@@ -20,7 +19,7 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
);
interface VoteBreakdownProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}
interface VoteProgressProps {
@@ -5,14 +5,13 @@ import { ProposalState } from '@vegaprotocol/types';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
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';
interface UserVoteProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
transaction: VegaTxState | null;
@@ -18,7 +18,6 @@ import { ProposalMinRequirements, ProposalUserAction } from '../shared';
import { VoteTransactionDialog } from './vote-transaction-dialog';
import { useVoteButtonsQuery } from './__generated__/Stake';
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
import { filterAcceptableGraphqlErrors } from '../../../../lib/party';
interface VoteButtonsContainerProps {
voteState: VoteState | null;
@@ -43,10 +42,8 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
skip: !pubKey,
});
const filteredErrors = filterAcceptableGraphqlErrors(error);
return (
<AsyncRenderer loading={loading} error={filteredErrors} data={data}>
<AsyncRenderer loading={loading} error={error} data={data}>
<VoteButtons
{...props}
currentStakeAvailable={toBigNum(
@@ -3,13 +3,12 @@ import {
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../../lib/bignumber';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
import { type Proposal } from '../types';
export const useProposalNetworkParams = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_updateMarket_requiredMajority,
@@ -2,15 +2,10 @@ import { useMemo } from 'react';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../lib/bignumber';
import { useProposalNetworkParams } from './use-proposal-network-params';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
import { addDecimal } from '@vegaprotocol/utils';
import { type Proposal } from '../types';
export const useVoteInformation = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
const {
appState: { totalSupply, decimals },
} = useAppState();
@@ -86,228 +86,65 @@ query Proposal(
$includeUpdateReferralProgram: Boolean!
) {
proposal(id: $proposalId) {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
... on Proposal {
id
}
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on SimpleRiskModel {
params {
factorLong
factorShort
}
}
}
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
}
}
}
}
}
}
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
... on SimpleRiskModel {
params {
factorLong
factorShort
}
quoteName
}
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
linearSlippageFactor
}
... on UpdateMarket {
marketId
updateMarketConfiguration {
instrument {
name
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
}
}
}
}
}
}
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
# 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
# }
# }
# }
# }
# }
# }
# }
quoteName
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
... on UpdatePerpetualProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
@@ -348,14 +185,19 @@ query Proposal(
}
}
}
dataSourceSpecBinding {
settlementDataProperty
settlementScheduleProperty
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
}
}
}
metadata
priceMonitoringParameters {
triggers {
horizonSecs
@@ -364,76 +206,239 @@ query Proposal(
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
positionDecimalPlaces
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
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
}
}
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
metadata
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
}
}
}
}
}
}
}
... on NewAsset {
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
... on NewAsset {
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
}
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
}
}
}
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
}
}
File diff suppressed because one or more lines are too long
@@ -17,6 +17,7 @@ import {
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { type Proposal as IProposal } from '../types';
export const ProposalContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
@@ -67,6 +68,8 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const proposal = data?.proposal as IProposal;
const successor = useSuccessorMarketProposalDetails(params.proposalId);
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
@@ -79,12 +82,12 @@ export const ProposalContainer = () => {
},
} = useFetch(
`${ENV.rest}governance?proposalId=${
data?.proposal?.terms.change.__typename === 'UpdateMarket' &&
data?.proposal.terms.change.marketId
proposal?.terms.change.__typename === 'UpdateMarket' &&
proposal.terms.change.marketId
}`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
@@ -97,7 +100,7 @@ export const ProposalContainer = () => {
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
@@ -108,8 +111,8 @@ export const ProposalContainer = () => {
dataProvider: marketInfoProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
marketId: proposal?.id || '',
skip: !proposal?.id,
},
});
@@ -148,23 +151,22 @@ export const ProposalContainer = () => {
fetchPolicy: 'network-only',
variables: {
assetId:
(data?.proposal?.terms.change.__typename === 'NewAsset' &&
data?.proposal?.id) ||
(data?.proposal?.terms.change.__typename === 'UpdateAsset' &&
data.proposal.terms.change.assetId) ||
(proposal?.terms.change.__typename === 'NewAsset' && proposal?.id) ||
(proposal?.terms.change.__typename === 'UpdateAsset' &&
proposal.terms.change.assetId) ||
'',
},
skip: !['NewAsset', 'UpdateAsset'].includes(
data?.proposal?.terms?.change?.__typename || ''
proposal?.terms?.change?.__typename || ''
),
});
useEffect(() => {
if (
previouslyEnactedMarketProposalsRestData &&
data?.proposal?.terms.change.__typename === 'UpdateMarket'
proposal?.terms.change.__typename === 'UpdateMarket'
) {
const change = data?.proposal?.terms?.change as { marketId: string };
const change = proposal?.terms?.change as { marketId: string };
const filteredProposals =
// @ts-ignore rest data is not typed
@@ -188,8 +190,8 @@ export const ProposalContainer = () => {
}, [
previouslyEnactedMarketProposalsRestData,
params.proposalId,
data?.proposal?.terms.change.__typename,
data?.proposal?.terms.change,
proposal?.terms.change.__typename,
proposal?.terms.change,
]);
useEffect(() => {
@@ -242,7 +244,7 @@ export const ProposalContainer = () => {
>
{data?.proposal ? (
<Proposal
proposal={data.proposal}
proposal={proposal}
networkParams={networkParams}
restData={restData}
marketData={marketData}
@@ -8,6 +8,7 @@ import mergeWith from 'lodash/mergeWith';
import { type PartialDeep } from 'type-fest';
import { type ProposalQuery } from '../proposal/__generated__/Proposal';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../types';
export function generateProtocolUpgradeProposal(
override: PartialDeep<ProtocolUpgradeProposalFieldsFragment> = {}
@@ -43,8 +44,8 @@ export function generateProtocolUpgradeProposal(
}
export function generateProposal(
override: PartialDeep<ProposalQuery['proposal']> = {}
): ProposalQuery['proposal'] {
override: PartialDeep<Proposal> = {}
): Proposal {
const defaultProposal: ProposalQuery['proposal'] = {
__typename: 'Proposal',
id: faker.datatype.uuid(),
@@ -92,15 +93,16 @@ export function generateProposal(
},
};
return mergeWith<
ProposalQuery['proposal'],
PartialDeep<ProposalQuery['proposal']>
>(defaultProposal, override, (objValue, srcValue) => {
if (!isArray(objValue)) {
return;
return mergeWith<Proposal, PartialDeep<Proposal>>(
defaultProposal,
override,
(objValue, srcValue) => {
if (!isArray(objValue)) {
return;
}
return srcValue;
}
return srcValue;
});
);
}
type Vote = Pick<Schema.Vote, '__typename' | 'value' | 'party' | 'datetime'>;
@@ -0,0 +1,11 @@
import type { ProposalQuery } from './proposal/__generated__/Proposal';
/**
* The default Proposal type needs extracting from the ProposalNode union type
* as lots of fields on the original type don't exist on BatchProposal. Eventually
* we will support BatchProposal but for now we don't
*/
export type Proposal = Extract<
ProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
@@ -10,7 +10,6 @@ import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
import { useNetworkParam } from '@vegaprotocol/network-parameters';
import { filterAcceptableGraphqlErrors } from '../../../lib/party';
const EPOCHS_PAGE_SIZE = 10;
@@ -100,24 +99,17 @@ export const EpochIndividualRewards = ({
prevEpochIdRef.current = epochId;
}, [epochId, refetchData]);
// Workarounds for the error handling of AsyncRenderer
const filteredErrors = filterAcceptableGraphqlErrors(error);
const filteredData = data || [];
return (
<AsyncRenderer
loading={loading}
error={filteredErrors}
data={filteredData}
error={error}
data={data}
render={() => (
<div>
<p data-testid="connected-vega-key" className="mb-10">
{t('Connected Vega key')}:{' '}
<span className="text-white">{pubKey}</span>
</p>
{epochIndividualRewardSummaries.length === 0 && (
<p>{t('No rewards for key')}</p>
)}
{epochIndividualRewardSummaries.map(
(epochIndividualRewardSummary) => (
<EpochIndividualRewardsTable
@@ -126,19 +118,17 @@ export const EpochIndividualRewards = ({
/>
)
)}
{epochIndividualRewardSummaries.length > 0 && (
<Pagination
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
>
{t('Page')} {page}
</Pagination>
)}
<Pagination
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
>
{t('Page')} {page}
</Pagination>
</div>
)}
/>
@@ -16,7 +16,7 @@ import type { ValidatorsView } from './validator-tables';
const nodeFactory = (
overrides?: PartialDeep<NodesFragmentFragment>
): NodesFragmentFragment => {
const defaultNode: NodesFragmentFragment = {
const defaultNode = {
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
name: 'high',
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
@@ -288,7 +288,7 @@ describe('Consensus validators table', () => {
expect(
grid.querySelector('[role="gridcell"][col-id="totalPenalties"]')
).toHaveTextContent('13.16%');
).toHaveTextContent('10.07%');
expect(
grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]')
@@ -185,15 +185,6 @@ export const ConsensusValidatorsTable = ({
const { rawValidatorScore: previousEpochValidatorScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakingPenalty = calculateOverallPenalty(
id,
allNodesInPreviousEpoch
);
const totalPenalty = calculateOverstakedPenalty(
id,
allNodesInPreviousEpoch
);
return {
id,
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
@@ -222,11 +213,11 @@ export const ConsensusValidatorsTable = ({
2
),
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
overstakingPenalty,
calculateOverstakedPenalty(id, allNodesInPreviousEpoch),
2
),
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
totalPenalty,
calculateOverallPenalty(id, allNodesInPreviousEpoch),
2
),
[ValidatorFields.PENDING_STAKE]: pendingStake,
@@ -124,15 +124,6 @@ export const StandbyPendingValidatorsTable = ({
}
}
const overstakingPenalty = calculateOverallPenalty(
id,
allNodesInPreviousEpoch
);
const totalPenalty = calculateOverstakedPenalty(
id,
allNodesInPreviousEpoch
);
return {
id,
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
@@ -163,11 +154,11 @@ export const StandbyPendingValidatorsTable = ({
2
),
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
overstakingPenalty,
calculateOverstakedPenalty(id, allNodesInPreviousEpoch),
2
),
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
totalPenalty,
calculateOverallPenalty(id, allNodesInPreviousEpoch),
2
),
[ValidatorFields.PENDING_STAKE]: pendingStake,
@@ -266,9 +266,7 @@ export const ValidatorTable = ({
<Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty">
{penalties.overstaked
? formatNumberPercentage(penalties.overstaked, 2)
: '-'}
{formatNumberPercentage(penalties.overstaked, 2)}
</span>
</Tooltip>
</KeyValueTableRow>
@@ -287,9 +285,7 @@ export const ValidatorTable = ({
</span>
<span data-testid="total-penalties">
<strong>
{penalties.overall
? formatNumberPercentage(penalties.overall, 2)
: '-'}
{formatNumberPercentage(penalties.overall, 2)}
</strong>
</span>
</KeyValueTableRow>
+45 -106
View File
@@ -3,11 +3,11 @@ import {
getLastEpochScoreAndPerformance,
getNormalisedVotingPower,
getUnnormalisedVotingPower,
getOverstakingPenalty,
getFormattedPerformanceScore,
getPerformancePenalty,
getTotalPenalties,
getStakePercentage,
calculateOverallPenalty,
calculateOverstakedPenalty,
} from './shared';
import * as Schema from '@vegaprotocol/types';
@@ -106,6 +106,38 @@ describe('getUnnormalisedVotingPower', () => {
});
});
describe('getOverstakingPenalty', () => {
it('returns "0%" when both arguments are null or undefined', () => {
expect(getOverstakingPenalty(null, null)).toBe('0%');
expect(getOverstakingPenalty(undefined, undefined)).toBe('0%');
expect(getOverstakingPenalty(null, undefined)).toBe('0%');
expect(getOverstakingPenalty(undefined, null)).toBe('0%');
});
it('returns "0%" when one argument is null or undefined', () => {
expect(getOverstakingPenalty('10', null)).toBe('0%');
expect(getOverstakingPenalty(null, '20')).toBe('0%');
expect(getOverstakingPenalty('10', undefined)).toBe('0%');
expect(getOverstakingPenalty(undefined, '20')).toBe('0%');
});
it('returns "0%" when validatorScore or stakeScore is zero', () => {
expect(getOverstakingPenalty('0', '20')).toBe('0%');
expect(getOverstakingPenalty('10', '0')).toBe('0%');
});
it('returns the correct overstaking penalty', () => {
expect(getOverstakingPenalty('0.18', '0.2')).toBe('10.00%');
expect(getOverstakingPenalty('0.2', '0.2')).toBe('0.00%');
expect(getOverstakingPenalty('0.04', '0.2')).toBe('80.00%');
});
it('handles string numbers with decimals', () => {
expect(getOverstakingPenalty('7.5', '15')).toBe('50.00%');
expect(getOverstakingPenalty('12.5', '25')).toBe('50.00%');
});
});
describe('getFormattedPerformanceScore', () => {
it('should return the formatted performance score', () => {
expect(getFormattedPerformanceScore('0.25')).toEqual(new BigNumber(0.25));
@@ -120,6 +152,17 @@ describe('getPerformancePenalty', () => {
});
});
describe('getTotalPenalties', () => {
it('should return the total penalties', () => {
expect(getTotalPenalties('0.25', '1', '5000', '10000')).toEqual('50.00%');
expect(getTotalPenalties('0.25', '0.5', '5000', '10000')).toEqual('75.00%');
});
it('should return 0 if the total penalties is negative', () => {
expect(getTotalPenalties('0.25', '0.5', '1000', '10000')).toEqual('0.00%');
});
});
describe('getStakePercentage', () => {
it('should return the stake percentage', () => {
expect(
@@ -139,107 +182,3 @@ describe('getStakePercentage', () => {
);
});
});
describe('calculateOverallPenalty', () => {
it('returns null if rewardScore is null', () => {
const res = calculateOverallPenalty('1', [
{
id: '1',
rewardScore: null,
stakedTotal: '',
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.75',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
previousStatus:
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
rankingScore: '',
votingPower: '',
},
},
]);
expect(res).toBeNull();
});
it('returns null if rewardScore.rawValidatorScore is null (should not happen)', () => {
const res = calculateOverallPenalty('1', [
{
id: '1',
stakedTotal: '',
rewardScore: {
rawValidatorScore: '0.25',
performanceScore: '0.75',
multisigScore: '',
validatorScore: null as unknown as string,
normalisedScore: '',
validatorStatus:
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.75',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
previousStatus:
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
rankingScore: '',
votingPower: '',
},
},
]);
expect(res).toBeNull();
});
});
describe('calculateOverstakedPenalty', () => {
it('returns null if rewardScore is null', () => {
const res = calculateOverstakedPenalty('1', [
{
id: '1',
rewardScore: null,
stakedTotal: '',
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.75',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
previousStatus:
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
rankingScore: '',
votingPower: '',
},
},
]);
expect(res).toBeNull();
});
it('returns null if rewardScore.rawValidatorScore is null (should not happen)', () => {
const res = calculateOverstakedPenalty('1', [
{
id: '1',
stakedTotal: '',
rewardScore: {
rawValidatorScore: null as unknown as string,
performanceScore: '0.75',
multisigScore: '',
validatorScore: '',
normalisedScore: '',
validatorStatus:
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.75',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
previousStatus:
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
rankingScore: '',
votingPower: '',
},
},
]);
expect(res).toBeNull();
});
});
+61 -35
View File
@@ -5,7 +5,6 @@ import {
import type { PreviousEpochQuery } from './__generated__/PreviousEpoch';
import { BigNumber } from '../../lib/bignumber';
import type { LastArrayElement } from 'type-fest';
import isNull from 'lodash/isNull';
type Node = NonNullable<
LastArrayElement<
@@ -22,10 +21,7 @@ type Node = NonNullable<
* @returns Theoretical stake score for given node based on the staked total
* of all node of the same type (status)
*/
const calculateTheoreticalStakeScore = (
nodeId: string,
nodes: Node[]
): BigNumber | null => {
const calculateTheoreticalStakeScore = (nodeId: string, nodes: Node[]) => {
const node = nodes.find((n) => n.id === nodeId);
if (!node) {
return new BigNumber(0);
@@ -46,25 +42,14 @@ const calculateTheoreticalStakeScore = (
* @param nodes A collection of all nodes - needed to calculate theoretical stake score
* @returns %
*/
export const calculateOverallPenalty = (
nodeId: string,
nodes: Node[]
): BigNumber | null => {
export const calculateOverallPenalty = (nodeId: string, nodes: Node[]) => {
const node = nodes.find((n) => n.id === nodeId);
const tts = calculateTheoreticalStakeScore(nodeId, nodes);
if (
!node ||
isNull(tts) ||
!node.rewardScore ||
(node.rewardScore && isNull(node.rewardScore.validatorScore))
) {
return null;
}
if (tts.isZero()) {
if (!node || tts.isZero()) {
return new BigNumber(0);
}
const penalty = new BigNumber(1)
.minus(new BigNumber(node.rewardScore.validatorScore).dividedBy(tts))
.minus(new BigNumber(node.rewardScore?.validatorScore || 0).dividedBy(tts))
.times(100);
return penalty.isLessThan(0) ? new BigNumber(0) : penalty;
};
@@ -75,21 +60,10 @@ export const calculateOverallPenalty = (
* @param nodes A collection of all nodes - needed to calculate theoretical stake score
* @returns %
*/
export const calculateOverstakedPenalty = (
nodeId: string,
nodes: Node[]
): BigNumber | null => {
export const calculateOverstakedPenalty = (nodeId: string, nodes: Node[]) => {
const node = nodes.find((n) => n.id === nodeId);
const tts = calculateTheoreticalStakeScore(nodeId, nodes);
if (
!node ||
isNull(tts) ||
isNull(node.rewardScore) ||
(node.rewardScore && node.rewardScore.rawValidatorScore === null)
) {
return null;
}
if (tts.isZero()) {
if (!node || tts.isZero()) {
return new BigNumber(0);
}
const penalty = new BigNumber(1)
@@ -104,9 +78,7 @@ export const calculateOverstakedPenalty = (
* Calculates performance penalty based on the given performance score.
* @returns %
*/
export const calculatesPerformancePenalty = (
performanceScore: string
): BigNumber => {
export const calculatesPerformancePenalty = (performanceScore: string) => {
const penalty = new BigNumber(1)
.minus(new BigNumber(performanceScore))
.times(100);
@@ -151,6 +123,60 @@ export const getPerformancePenalty = (performanceScore?: string) =>
2
);
export const getOverstakingPenalty = (
validatorScore: string | null | undefined,
stakeScore: string | null | undefined
) => {
if (!validatorScore || !stakeScore) {
return '0%';
}
// avoid division by zero
if (
new BigNumber(validatorScore).isZero() ||
new BigNumber(stakeScore).isZero()
) {
return '0%';
}
return formatNumberPercentage(
BigNumber.max(
new BigNumber(1)
.minus(
new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore))
)
.times(100),
new BigNumber(0)
),
2
);
};
export const getTotalPenalties = (
rawValidatorScore: string | null | undefined,
performanceScore: string | undefined,
stakedOnNode: string,
totalStake: string
) => {
const calc =
rawValidatorScore &&
performanceScore &&
new BigNumber(totalStake).isGreaterThan(0)
? new BigNumber(1).minus(
new BigNumber(performanceScore)
.times(new BigNumber(rawValidatorScore))
.dividedBy(
new BigNumber(stakedOnNode).dividedBy(new BigNumber(totalStake))
)
)
: new BigNumber(0);
return formatNumberPercentage(
calc.isPositive() ? calc.times(100) : new BigNumber(0),
2
);
};
export const getStakePercentage = (total: BigNumber, stakedOnNode: BigNumber) =>
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
? '0%'
+1 -37
View File
@@ -1,50 +1,14 @@
import {
Intent,
ToastsContainer,
TradingButton,
VegaIcon,
VegaIconNames,
useToasts,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import {
useEthereumTransactionToasts,
useEthereumWithdrawApprovalsToasts,
useVegaTransactionToasts,
useWalletDisconnectToastActions,
useWalletDisconnectedToasts,
} from '@vegaprotocol/web3';
import { useTranslation } from 'react-i18next';
const WalletDisconnectAdditionalContent = () => {
const { t } = useTranslation();
const { hideToast } = useWalletDisconnectToastActions();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<p className="mt-2">
<TradingButton
data-testid="connect-vega-wallet"
onClick={() => {
hideToast();
openVegaWalletDialog();
}}
size="small"
intent={Intent.Danger}
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
>
<span className="whitespace-nowrap uppercase">{t('Connect')}</span>
</TradingButton>
</p>
);
};
export const ToastsManager = () => {
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
useWalletDisconnectedToasts(<WalletDisconnectAdditionalContent />);
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
+2 -7
View File
@@ -22,12 +22,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
# NX_DISABLE_CLOSE_POSITION=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
NX_TEAM_COMPETITION=true
-3
View File
@@ -28,6 +28,3 @@ NX_REFERRALS=true
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
# NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
@@ -314,7 +314,7 @@ describe('Closed', () => {
});
it('display market actions', async () => {
// Use market with a successor Id as the actions dropdown will optionally
// Use market with a succcessor Id as the actions dropdown will optionally
// show a link to the successor market
const marketsWithSuccessorAndParent = [
{
@@ -137,8 +137,6 @@ const ClosedMarketsDataGrid = ({
headerName: t('Market'),
field: 'code',
cellRenderer: 'MarketCodeCell',
width: 150,
resizable: true,
},
{
headerName: t('Status'),
@@ -282,7 +280,6 @@ const ClosedMarketsDataGrid = ({
return (
<AgGrid
rowData={rowData}
defaultColDef={COL_DEFS.default}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
@@ -17,7 +17,6 @@ const defaultColDef = {
filter: true,
resizable: true,
filterParams: { buttons: ['reset'] },
minWidth: 120,
};
const components = {
@@ -53,7 +53,6 @@ export const MarketsPage = () => {
size="extra-small"
data-testid="propose-new-market"
href={externalLink}
target="_blank"
>
{t('Propose a new market')}
</TradingAnchorButton>
@@ -29,7 +29,6 @@ export const useColumnDefs = () => {
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
pinned: true,
cellRenderer: ({
value,
data,
@@ -0,0 +1,89 @@
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
+151
View File
@@ -0,0 +1,151 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean };
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } };
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
export type TeamQueryVariables = Types.Exact<{
teamId: Types.Scalars['ID'];
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
export const TeamFieldsFragmentDoc = gql`
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
`;
export const TeamStatsFieldsFragmentDoc = gql`
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
`;
export const TeamRefereeFieldsFragmentDoc = gql`
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
`;
export const TeamEntityFragmentDoc = gql`
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
`;
export const TeamGameFieldsFragmentDoc = gql`
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
${TeamEntityFragmentDoc}`;
export const TeamDocument = gql`
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
${TeamFieldsFragmentDoc}
${TeamStatsFieldsFragmentDoc}
${TeamRefereeFieldsFragmentDoc}
${TeamGameFieldsFragmentDoc}`;
/**
* __useTeamQuery__
*
* To run a query within a React component, call `useTeamQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamQuery` 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 } = useTeamQuery({
* variables: {
* teamId: // value for 'teamId'
* partyId: // value for 'partyId'
* },
* });
*/
export function useTeamQuery(baseOptions: Apollo.QueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export function useTeamLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export type TeamQueryHookResult = ReturnType<typeof useTeamQuery>;
export type TeamLazyQueryHookResult = ReturnType<typeof useTeamLazyQuery>;
export type TeamQueryResult = Apollo.QueryResult<TeamQuery, TeamQueryVariables>;
+1
View File
@@ -0,0 +1 @@
export { Team } from './team';
+395
View File
@@ -0,0 +1,395 @@
import { useState, type ReactNode, type ButtonHTMLAttributes } from 'react';
import { Link, useParams } from 'react-router-dom';
import orderBy from 'lodash/orderBy';
import countBy from 'lodash/countBy';
import {
TradingButton as Button,
Intent,
Pill,
VegaIcon,
VegaIconNames,
Tooltip,
Splash,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
import { formatNumberRounded, getDateTimeFormat } from '@vegaprotocol/utils';
import {
useTeam,
type Team as TeamType,
type TeamStats,
type Member,
type TeamGame,
} from './use-team';
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
import BigNumber from 'bignumber.js';
export const Team = () => {
const t = useT();
const { teamId } = useParams<{ teamId: string }>();
const { team, stats, partyInTeam, members, games } = useTeam(teamId);
if (!team) {
return (
<Splash>
<p>{t('Page not found')}</p>
</Splash>
);
}
return (
<TeamPage
team={team}
stats={stats}
partyInTeam={partyInTeam}
members={members}
games={games}
/>
);
};
export const TeamPage = ({
team,
stats,
partyInTeam,
members,
games,
}: {
team: TeamType;
stats?: TeamStats;
partyInTeam: boolean;
members?: Member[];
games?: TeamGame[];
}) => {
const t = useT();
const [showGames, setShowGames] = useState(true);
return (
<div className="relative h-full overflow-y-auto">
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
</div>
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
<header className="flex gap-3 lg:gap-4 pt-5 lg:pt-10">
<TeamAvatar imgUrl={team.avatarUrl} />
<div className="flex flex-col items-start gap-1 lg:gap-3">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{team.name}
</h1>
<JoinButton joined={partyInTeam} />
</div>
</header>
<StatSection>
<StatList>
<Stat value={members ? members.length : 0} label={t('Members')} />
<Stat
value={stats ? stats.totalGamesPlayed : 0}
label={t('Total games')}
tooltip={t('Total number of games this team has participated in')}
/>
<StatSectionSeparator />
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumVolume),
'1e3'
)
: 0
}
label={t('Total volume')}
/>
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumRewards),
'1e3'
)
: 0
}
label={t('Rewards paid')}
tooltip={'Total amount of rewards paid out to this team in qUSD'}
/>
</StatList>
</StatSection>
{games && games.length ? (
<StatSection>
<FavoriteGame games={games} />
<StatSectionSeparator />
<LatestResults games={games} />
</StatSection>
) : null}
<section>
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
<ToggleButton active={showGames} onClick={() => setShowGames(true)}>
{t('Games ({{count}})', { count: games ? games.length : 0 })}
</ToggleButton>
<ToggleButton
active={!showGames}
onClick={() => setShowGames(false)}
>
{t('Members ({{count}})', {
count: members ? members.length : 0,
})}
</ToggleButton>
</div>
{showGames ? <Games games={games} /> : <Members members={members} />}
</section>
</div>
</div>
);
};
const Games = ({ games }: { games?: TeamGame[] }) => {
const t = useT();
if (!games?.length) {
return <p>{t('No games')}</p>;
}
return (
<Table
columns={[
{ name: 'rank', displayName: t('Rank') },
{
name: 'epoch',
displayName: t('Epoch'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('Amount earned') },
{
name: 'teams',
displayName: t('No. of participating teams'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'status', displayName: t('Status') },
]}
data={games.map((game) => ({
rank: game.team.rank,
epoch: game.epoch,
type: game.team.rewardMetric,
amount: game.team.totalRewardsEarned,
teams: game.numberOfParticipants,
}))}
noCollapse={true}
/>
);
};
const TeamAvatar = ({ imgUrl }: { imgUrl: string }) => {
// TODO: add fallback avatars
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imgUrl}
alt="Team avatar"
className="rounded-full w-20 h-20 lg:w-[112px] lg:h-[112px] bg-vega-clight-700 dark:bg-vega-cdark-700 shrink-0"
/>
);
};
const Members = ({ members }: { members?: Member[] }) => {
const t = useT();
if (!members?.length) {
return <p>{t('No members')}</p>;
}
const data = orderBy(
members.map((m) => ({
referee: <RefereeCell pubkey={m.referee} />,
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch),
explorerLink: <RefereeLink pubkey={m.referee} />,
})),
'joinedAtEpoch',
'desc'
);
return (
<Table
columns={[
{ name: 'referee', displayName: t('Referee') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
headerClassName: 'text-right',
className: 'text-right',
},
{
name: 'explorerLink',
displayName: '',
headerClassName: 'hidden md:block',
className: 'hidden md:block text-right',
},
]}
data={data}
noCollapse={true}
/>
);
};
const RefereeCell = ({ pubkey }: { pubkey: string }) => {
return <span title={pubkey}>{truncateMiddle(pubkey)}</span>;
};
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
const t = useT();
const linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return (
<Link to={link} className="underline underline-offset-4">
{t('View on explorer')}
</Link>
);
};
const JoinButton = ({ joined }: { joined: boolean }) => {
const t = useT();
if (joined) {
return (
<Button intent={Intent.None} disabled={true}>
<span className="flex items-center gap-2">
{t('Joined')}{' '}
<span className="text-vega-green-600 dark:text-vega-green">
<VegaIcon name={VegaIconNames.TICK} />
</span>
</span>
</Button>
);
}
return <Button intent={Intent.Primary}>{t('Join this team')}</Button>;
};
const LatestResults = ({ games }: { games: TeamGame[] }) => {
const t = useT();
const latestGames = games.slice(0, 5);
return (
<dl>
<dt className="text-muted text-sm">
{t('Last {{count}} game results', { count: latestGames.length })}
</dt>
<dd className="flex gap-1">
{latestGames.map((game) => {
return (
<Pill key={game.id} className="text-sm">
{t('place', { count: game.team.rank, ordinal: true })}
</Pill>
);
})}
</dd>
</dl>
);
};
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
const t = useT();
const rewardMetrics = games.map((game) => game.team.rewardMetric);
const count = countBy(rewardMetrics);
let favoriteMetric = '';
let mostOccurances = 0;
for (const key in count) {
if (count[key] > mostOccurances) {
favoriteMetric = key;
mostOccurances = count[key];
}
}
if (!favoriteMetric) return null;
return (
<dl>
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
<dd>
<Pill className="flex-inline items-center gap-2 bg-transparent text-sm">
<VegaIcon
name={VegaIconNames.STAR}
className="text-vega-yellow-400"
/>{' '}
{favoriteMetric}
</Pill>
</dd>
</dl>
);
};
const ToggleButton = ({
active,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean }) => {
return (
<button
{...props}
className={classNames('relative top-px uppercase border-b-2 py-4', {
'text-muted border-transparent': !active,
'border-vega-yellow': active,
})}
/>
);
};
const StatSection = ({ children }: { children: ReactNode }) => {
return (
<section className="flex flex-col lg:flex-row gap-2 lg:gap-8">
{children}
</section>
);
};
const StatSectionSeparator = () => {
return <div className="hidden md:block border-r border-default" />;
};
const StatList = ({ children }: { children: ReactNode }) => {
return (
<dl className="grid grid-cols-[min-content_min-content] md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
{children}
</dl>
);
};
const Stat = ({
value,
label,
tooltip,
}: {
value: ReactNode;
label: ReactNode;
tooltip?: string;
}) => {
return (
<div>
<dd className="text-3xl lg:text-4xl">{value}</dd>
<dt className="text-sm text-muted">
{tooltip ? (
<Tooltip description={tooltip} underline={false}>
<span className="flex items-center gap-2">
{label}
<VegaIcon name={VegaIconNames.INFO} size={12} />
</span>
</Tooltip>
) : (
label
)}
</dt>
</div>
);
};
@@ -0,0 +1,64 @@
import compact from 'lodash/compact';
import orderBy from 'lodash/orderBy';
import {
useTeamQuery,
type TeamFieldsFragment,
type TeamStatsFieldsFragment,
type TeamRefereeFieldsFragment,
type TeamEntityFragment,
} from './__generated__/Team';
import { useVegaWallet } from '@vegaprotocol/wallet';
export type Team = TeamFieldsFragment;
export type TeamStats = TeamStatsFieldsFragment;
export type Member = TeamRefereeFieldsFragment;
export type TeamEntity = TeamEntityFragment;
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
export const useTeam = (teamId?: string) => {
const { pubKey } = useVegaWallet();
const { data, loading, error } = useTeamQuery({
variables: { teamId: teamId || '', partyId: pubKey },
skip: !teamId,
});
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
const partyTeamEdge = data?.partyTeams?.edges[0];
const teamStatsEdge = data?.teamsStatistics?.edges.find(
(e) => e.node.teamId === teamId
);
const members = data?.teamReferees?.edges
.filter((e) => e.node.teamId === teamId)
.map((e) => e.node);
// Find games where the current team participated in
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
const team = edge.node.entities.find((e) => {
if (e.__typename !== 'TeamGameEntity') return false;
if (e.team.teamId !== teamId) return false;
return true;
});
if (!team) return null;
return {
id: edge.node.id,
epoch: edge.node.epoch,
numberOfParticipants: edge.node.numberOfParticipants,
team: team as TeamEntity, // TS can't infer that all the game entities are teams
};
});
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
return {
data,
loading,
error,
stats: teamStatsEdge?.node,
team: teamEdge?.node,
members,
games,
partyInTeam: Boolean(partyTeamEdge),
};
};
-1
View File
@@ -1 +0,0 @@
export { Teams } from './teams';
@@ -1,7 +0,0 @@
export const Teams = () => {
return (
<div>
<h1>Teams</h1>
</div>
);
};
@@ -13,7 +13,6 @@ import type { ReactNode } from 'react';
import { Web3Provider } from './web3-provider';
import { useT } from '../../lib/use-t';
import { DataLoader } from './data-loader';
import { useChainId } from '@vegaprotocol/wallet';
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
const t = useT();
@@ -27,16 +26,13 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
CHROME_EXTENSION_URL,
} = useEnvironment();
const chainId = useChainId(VEGA_URL);
if (
!VEGA_URL ||
!VEGA_WALLET_URL ||
!VEGA_EXPLORER_URL ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL ||
!DocsLinks ||
!chainId
!DocsLinks
) {
return <AppLoader />;
}
@@ -76,7 +72,6 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
chainId,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
@@ -1,5 +1,5 @@
import invert from 'lodash/invert';
import { type Interval } from '@vegaprotocol/types';
import { Interval } from '@vegaprotocol/types';
import {
TradingViewContainer,
ALLOWED_TRADINGVIEW_HOSTNAMES,
@@ -11,6 +11,7 @@ import {
} from '@vegaprotocol/candles-chart';
import { useEnvironment } from '@vegaprotocol/environment';
import { useChartSettings, STUDY_SIZE } from './use-chart-settings';
import { SUPPORTED_INTERVALS, type SupportedInterval } from './constants';
/**
* Renders either the pennant chart or the tradingview chart
@@ -25,18 +26,18 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
overlays,
studies,
studySizes,
tradingViewStudies,
setInterval,
setStudies,
setStudySizes,
setOverlays,
state,
setState,
setTradingViewStudies,
} = useChartSettings();
const pennantChart = (
<CandlesChartContainer
marketId={marketId}
interval={toPennantInterval(interval)}
interval={toPennantInterval(interval as SupportedInterval)}
chartType={chartType}
overlays={overlays}
studies={studies}
@@ -63,14 +64,14 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
libraryPath={CHARTING_LIBRARY_PATH}
libraryHash={CHARTING_LIBRARY_HASH}
marketId={marketId}
interval={toTradingViewResolution(interval)}
interval={toTradingViewResolution(interval as SupportedInterval)}
studies={tradingViewStudies}
onIntervalChange={(newInterval) => {
setInterval(fromTradingViewResolution(newInterval));
}}
onAutoSaveNeeded={(data) => {
setState(data);
onAutoSaveNeeded={(data: { studies: string[] }) => {
setTradingViewStudies(data.studies);
}}
state={state}
/>
);
}
@@ -83,7 +84,11 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
}
};
const toTradingViewResolution = (interval: Interval) => {
const toTradingViewResolution = (interval: SupportedInterval) => {
if (!SUPPORTED_INTERVALS.includes(interval)) {
throw new Error(`interval ${interval} is not supported`);
}
const resolution = TRADINGVIEW_INTERVAL_MAP[interval];
if (!resolution) {
@@ -107,7 +112,11 @@ const fromTradingViewResolution = (resolution: string) => {
return interval as Interval;
};
const toPennantInterval = (interval: Interval) => {
const toPennantInterval = (interval: SupportedInterval) => {
if (!SUPPORTED_INTERVALS.includes(interval)) {
throw new Error(`interval ${interval} is not supported`);
}
const pennantInterval = PENNANT_INTERVAL_MAP[interval];
if (!pennantInterval) {
@@ -27,10 +27,10 @@ describe('ChartMenu', () => {
render(<ChartMenu />);
await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
await userEvent.click(screen.getByRole('button', { name: 'TradingView' }));
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
await userEvent.click(screen.getByRole('button', { name: 'Vega chart' }));
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
});
@@ -24,15 +24,7 @@ import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
import { IconNames, type IconName } from '@blueprintjs/icons';
import { useChartSettings } from './use-chart-settings';
import { useT } from '../../lib/use-t';
const INTERVALS = [
Interval.INTERVAL_I1M,
Interval.INTERVAL_I5M,
Interval.INTERVAL_I15M,
Interval.INTERVAL_I1H,
Interval.INTERVAL_I6H,
Interval.INTERVAL_I1D,
];
import { SUPPORTED_INTERVALS } from './constants';
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
@@ -68,7 +60,6 @@ export const ChartMenu = () => {
setChartlib(isPennant ? 'tradingview' : 'pennant');
}}
size="extra-small"
testId="chartlib-toggle-button"
>
{isPennant ? 'TradingView' : t('Vega chart')}
</TradingButton>
@@ -94,7 +85,7 @@ export const ChartMenu = () => {
setInterval(value as Interval);
}}
>
{INTERVALS.map((timeInterval) => (
{SUPPORTED_INTERVALS.map((timeInterval) => (
<TradingDropdownRadioItem
key={timeInterval}
inset
@@ -0,0 +1,12 @@
import { Interval } from '@vegaprotocol/types';
export type SupportedInterval = typeof SUPPORTED_INTERVALS[number];
export const SUPPORTED_INTERVALS = [
Interval.INTERVAL_I1M,
Interval.INTERVAL_I5M,
Interval.INTERVAL_I15M,
Interval.INTERVAL_I1H,
Interval.INTERVAL_I6H,
Interval.INTERVAL_I1D,
] as const;
@@ -9,7 +9,6 @@ type StudySizes = { [S in Study]?: number };
export type Chartlib = 'pennant' | 'tradingview';
interface StoredSettings {
state: object | undefined; // Don't see a better type provided from TradingView type definitions
chartlib: Chartlib;
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
// chart types easier and more consistent
@@ -18,6 +17,7 @@ interface StoredSettings {
overlays: Overlay[];
studies: Study[];
studySizes: StudySizes;
tradingViewStudies: string[];
}
export const STUDY_SIZE = 90;
@@ -30,13 +30,13 @@ const STUDY_ORDER: Study[] = [
];
export const DEFAULT_CHART_SETTINGS = {
state: undefined,
chartlib: 'pennant' as const,
interval: Interval.INTERVAL_I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
studySizes: {},
tradingViewStudies: ['Volume'],
};
export const useChartSettingsStore = create<
@@ -47,7 +47,7 @@ export const useChartSettingsStore = create<
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
setChartlib: (lib: Chartlib) => void;
setState: (state: object) => void;
setTradingViewStudies: (studies: string[]) => void;
}
>()(
persist(
@@ -95,8 +95,10 @@ export const useChartSettingsStore = create<
state.chartlib = lib;
});
},
setState: (state) => {
set({ state });
setTradingViewStudies: (studies: string[]) => {
set((state) => {
state.tradingViewStudies = studies;
});
},
})),
{
@@ -145,13 +147,13 @@ export const useChartSettings = () => {
overlays,
studies,
studySizes,
tradingViewStudies: settings.tradingViewStudies,
setInterval: settings.setInterval,
setType: settings.setType,
setStudies: settings.setStudies,
setOverlays: settings.setOverlays,
setStudySizes: settings.setStudySizes,
setChartlib: settings.setChartlib,
state: settings.state,
setState: settings.setState,
setTradingViewStudies: settings.setTradingViewStudies,
};
};
@@ -1,21 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GridSettings } from './grid-settings';
import '@testing-library/jest-dom';
describe('GridSettings', () => {
it('calls updateGridStore with correct arguments on button click', async () => {
const mockUpdateGridStore = jest.fn();
const { getByText } = render(
<GridSettings updateGridStore={mockUpdateGridStore} />
);
await userEvent.click(getByText('Reset Columns'));
expect(mockUpdateGridStore).toHaveBeenCalledWith({
columnState: undefined,
filterModel: undefined,
});
});
});
@@ -42,9 +42,13 @@ export const LiquidityHeader = () => {
const assetDecimalPlaces = asset?.decimals || 0;
const symbol = asset?.symbol;
const triggeringRatio =
market?.liquidityMonitoringParameters.triggeringRatio || '1';
const { percentage, status } = useCheckLiquidityStatus({
suppliedStake: suppliedStake || 0,
targetStake: targetStake || 0,
triggeringRatio,
});
const feesObject = feesPaidRes?.paidLiquidityFees?.edges?.find(
@@ -47,6 +47,9 @@ export const MarketLiquiditySupplied = ({
]);
const stakeToCcyVolume = params.market_liquidity_stakeToCcyVolume;
const triggeringRatio = Number(
params.market_liquidity_targetstake_triggering_ratio
);
const variables = useMemo(
() => ({
@@ -91,6 +94,7 @@ export const MarketLiquiditySupplied = ({
const { percentage, status } = useCheckLiquidityStatus({
suppliedStake: market?.suppliedStake || 0,
targetStake: market?.targetStake || 0,
triggeringRatio,
});
const showMessage =
+1 -10
View File
@@ -162,8 +162,6 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
const envNameMapping = useEnvNameMapping();
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
const marketId = useGlobalStore((store) => store.marketId);
const GOVERNANCE_LINK = useLinks(DApp.Governance)();
const EXPLORER_LINK = useLinks(DApp.Explorer)();
return (
<div className="gap-3 lg:flex lg:h-full">
@@ -222,7 +220,7 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLinkExternal to={GOVERNANCE_LINK}>
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
{t('Governance')}
</NavbarLinkExternal>
</NavbarItem>
@@ -230,13 +228,6 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
<NavbarContent data-testid="navbar-content-resources">
<ul className="lg:p-4">
{EXPLORER_LINK && (
<NavbarSubItem>
<NavbarLinkExternal to={EXPLORER_LINK}>
{t('Explorer')}
</NavbarLinkExternal>
</NavbarSubItem>
)}
{DocsLinks?.NEW_TO_VEGA && (
<NavbarSubItem>
<NavbarLinkExternal to={DocsLinks?.NEW_TO_VEGA}>
@@ -98,7 +98,6 @@ describe('ActiveRewards', () => {
transferNode={mockTransferNode}
currentEpoch={1}
kind={mockRecurringTransfer}
allMarkets={{}}
/>
);

Some files were not shown because too many files have changed in this diff Show More