Compare commits

..
Author SHA1 Message Date
Madalina Raicu c1083e502b fix(trading): order trading.json 2024-01-17 19:17:38 +00:00
132 changed files with 1875 additions and 21465 deletions
+3 -3
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: 90
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: 90
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 2 --dist loadfile --durations=90
working-directory: apps/trading/e2e
#----------------------------------------------
# upload traces
+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)
@@ -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,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',
@@ -242,7 +241,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',
+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;
}
@@ -124,7 +124,7 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description')
).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update to market ID: MarketId'
'Market change: MarketId'
);
});
@@ -1,11 +1,5 @@
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';
@@ -18,12 +12,7 @@ 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';
@@ -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 = '';
@@ -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;
@@ -48,7 +48,6 @@ const vegaWalletConfig: VegaWalletConfig = {
chromeExtensionUrl: 'chrome',
mozillaExtensionUrl: 'mozilla',
},
chainId: 'VEGA_CHAIN_ID',
};
const renderComponent = (proposal: ProposalQuery['proposal']) => {
@@ -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(
@@ -212,6 +212,7 @@ query Proposal(
}
positionDecimalPlaces
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
marketId
File diff suppressed because one or more lines are too long
@@ -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
+2 -2
View File
@@ -29,5 +29,5 @@ 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=
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
@@ -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,
@@ -25,12 +25,12 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
overlays,
studies,
studySizes,
tradingViewStudies,
setInterval,
setStudies,
setStudySizes,
setOverlays,
state,
setState,
setTradingViewStudies,
} = useChartSettings();
const pennantChart = (
@@ -64,13 +64,13 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
libraryHash={CHARTING_LIBRARY_HASH}
marketId={marketId}
interval={toTradingViewResolution(interval)}
studies={tradingViewStudies}
onIntervalChange={(newInterval) => {
setInterval(fromTradingViewResolution(newInterval));
}}
onAutoSaveNeeded={(data) => {
setState(data);
onAutoSaveNeeded={(data: { studies: string[] }) => {
setTradingViewStudies(data.studies);
}}
state={state}
/>
);
}
@@ -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');
});
@@ -68,7 +68,6 @@ export const ChartMenu = () => {
setChartlib(isPennant ? 'tradingview' : 'pennant');
}}
size="extra-small"
testId="chartlib-toggle-button"
>
{isPennant ? 'TradingView' : t('Vega chart')}
</TradingButton>
@@ -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,
});
});
});
+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={{}}
/>
);
@@ -1,46 +1,48 @@
import { useActiveRewardsQuery } from './__generated__/Rewards';
import {
useActiveRewardsQuery,
useMarketForRewardsQuery,
} from './__generated__/Rewards';
import { useT } from '../../lib/use-t';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import classNames from 'classnames';
import {
type IconName,
type VegaIconSize,
Icon,
type IconName,
Intent,
Tooltip,
VegaIcon,
VegaIconNames,
type VegaIconSize,
TradingInput,
TinyScroll,
} from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import {
type Maybe,
type Transfer,
type TransferNode,
type RecurringTransfer,
DistributionStrategyDescriptionMapping,
DistributionStrategyMapping,
EntityScope,
EntityScopeMapping,
type Maybe,
type Transfer,
type TransferNode,
TransferStatus,
TransferStatusMapping,
DispatchMetric,
DispatchMetricDescription,
DispatchMetricLabels,
type RecurringTransfer,
EntityScopeLabelMapping,
MarketState,
} from '@vegaprotocol/types';
import { Card } from '../card/card';
import { useMemo, useState } from 'react';
import {
type AssetFieldsFragment,
useAssetDataProvider,
useAssetsMapProvider,
} from '@vegaprotocol/assets';
import {
type MarketFieldsFragment,
useMarketsMapProvider,
getAsset,
} from '@vegaprotocol/markets';
export type Filter = {
@@ -72,7 +74,7 @@ export const isActiveReward = (node: TransferNode, currentEpoch: number) => {
export const applyFilter = (
node: TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
marketIds?: (MarketFieldsFragment | null)[];
},
filter: Filter
) => {
@@ -83,7 +85,6 @@ export const applyFilter = (
) {
return false;
}
if (
DispatchMetricLabels[transfer.kind.dispatchStrategy.dispatchMetric]
.toLowerCase()
@@ -97,7 +98,7 @@ export const applyFilter = (
node.asset?.name
.toLocaleLowerCase()
.includes(filter.searchTerm.toLowerCase()) ||
node.markets?.some((m) =>
node.marketIds?.some((m) =>
m?.tradableInstrument?.instrument?.name
.toLocaleLowerCase()
.includes(filter.searchTerm.toLowerCase())
@@ -123,7 +124,7 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
const enrichedTransfers = activeRewardsData?.transfersConnection?.edges
const transfers = activeRewardsData?.transfersConnection?.edges
?.map((e) => e?.node as TransferNode)
.filter((node) => isActiveReward(node, currentEpoch))
.map((node) => {
@@ -137,19 +138,19 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
const marketIds =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
return { ...node, asset, marketIds };
});
if (!enrichedTransfers || !enrichedTransfers.length) return null;
if (!transfers || !transfers.length) return null;
return (
<Card title={t('Active rewards')} className="lg:col-span-full">
{enrichedTransfers.length > 1 && (
{transfers.length > 1 && (
<TradingInput
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
@@ -165,7 +166,7 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
/>
)}
<TinyScroll className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] max-h-[40rem] overflow-auto pr-2">
{enrichedTransfers
{transfers
.filter((n) => applyFilter(n, filter))
.map((node, i) => {
const { transfer } = node;
@@ -183,7 +184,6 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
transferNode={node}
kind={transfer.kind}
currentEpoch={currentEpoch}
allMarkets={markets || {}}
/>
)
);
@@ -207,8 +207,14 @@ const StatusIndicator = ({
switch (status) {
case TransferStatus.STATUS_DONE:
return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
case TransferStatus.STATUS_CANCELLED:
return { icon: IconNames.MOON, intent: Intent.None };
case TransferStatus.STATUS_PENDING:
return { icon: IconNames.HELP, intent: Intent.Primary };
case TransferStatus.STATUS_REJECTED:
return { icon: IconNames.ERROR, intent: Intent.Danger };
case TransferStatus.STATUS_STOPPED:
return { icon: IconNames.ERROR, intent: Intent.Danger };
default:
return { icon: IconNames.HELP, intent: Intent.Primary };
}
@@ -247,97 +253,49 @@ export const ActiveRewardCard = ({
transferNode,
currentEpoch,
kind,
allMarkets,
}: {
transferNode: TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
};
transferNode: TransferNode;
currentEpoch: number;
kind: RecurringTransfer;
allMarkets?: Record<string, MarketFieldsFragment | null>;
}) => {
const t = useT();
const { transfer } = transferNode;
const { dispatchStrategy } = kind;
const marketIds = dispatchStrategy?.marketIdsInScope;
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
const firstMarketData = transferNode.markets?.[0];
const { data: marketNameData } = useMarketForRewardsQuery({
variables: {
marketId: marketIds ? marketIds[0] : '',
},
});
const specificMarkets = useMemo(() => {
if (
!firstMarketData ||
!marketIdsInScope ||
marketIdsInScope.length === 0
const marketName = useMemo(() => {
if (marketNameData && marketIds && marketIds.length > 1) {
return 'Specific markets';
} else if (
marketNameData &&
marketIds &&
marketNameData &&
marketIds.length === 1
) {
return null;
return marketNameData?.market?.tradableInstrument?.instrument?.name || '';
}
if (marketIdsInScope.length > 1) {
const marketNames =
allMarkets &&
marketIdsInScope
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
.join(', ');
return '';
}, [marketIds, marketNameData]);
return (
<Tooltip description={marketNames}>
<span>Specific markets</span>
</Tooltip>
);
}
return (
<span>{firstMarketData?.tradableInstrument?.instrument?.name || ''}</span>
);
}, [firstMarketData, marketIdsInScope, allMarkets]);
const dispatchAsset = transferNode.asset;
const { data: dispatchAsset } = useAssetDataProvider(
dispatchStrategy?.dispatchMetricAssetId || ''
);
if (!dispatchStrategy) {
return null;
}
// Gray out/hide the cards that are related to not trading markets
const marketSettled = transferNode.markets?.some(
(m) =>
m?.state &&
[
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_SETTLED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(m.state)
const { gradientClassName, mainClassName } = getGradientClasses(
dispatchStrategy.dispatchMetric
);
if (marketSettled) {
return null;
}
const assetInActiveMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return m?.state && MarketState.STATE_ACTIVE === m.state;
}
return false;
});
const marketSuspended = transferNode.markets?.some(
(m) =>
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
// Gray out the cards that are related to suspended markets
// Or settlement assets in markets that are not active and eligible for rewards
const { gradientClassName, mainClassName } =
marketSuspended || !assetInActiveMarket
? {
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
}
: getGradientClasses(dispatchStrategy.dispatchMetric);
const entityScope = dispatchStrategy.entityScope;
return (
<div>
@@ -415,20 +373,8 @@ export const ActiveRewardCard = ({
<span className="border-[0.5px] border-gray-700" />
<span>
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<Tooltip
underline={marketSuspended}
description={
(marketSuspended || !assetInActiveMarket) &&
(specificMarkets
? t('Eligible market(s) currently suspended')
: !assetInActiveMarket
? t('Currently no markets eligible for reward')
: '')
}
>
<span>{specificMarkets || dispatchAsset?.name}</span>
</Tooltip>
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
{marketName ? `${marketName}` : `${dispatchAsset?.name}`}
</span>
<div className="flex items-center gap-8 flex-wrap">
@@ -81,7 +81,6 @@ export const Settings = () => {
intent={Intent.Primary}
onClick={() => {
localStorage.clear();
sessionStorage.clear();
window.location.reload();
}}
>
@@ -14,18 +14,15 @@ market_order = "order-type-Market"
tif = "order-tif"
expire = "expire"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def continuous_market(vega):
return setup_continuous_market(vega)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_limit_buy_order_GTT(continuous_market, vega: VegaServiceNull, page: Page):
page.goto(f"/#/markets/{continuous_market}")
@@ -45,26 +42,31 @@ def test_limit_buy_order_GTT(continuous_market, vega: VegaServiceNull, page: Pag
)
page.get_by_test_id(place_order).click()
wait_for_toast_confirmation(page)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.get_by_test_id("All").click()
# 7002-SORD-017
expect(page.get_by_role("row").nth(5)).to_contain_text("10+10LimitFilled120.00GTT:")
expect(page.get_by_role("row").nth(5)).to_contain_text(
"10+10LimitFilled120.00GTT:"
)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_limit_buy_order(continuous_market, vega: VegaServiceNull, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.get_by_test_id(place_order).click()
wait_for_toast_confirmation(page)
vega.wait_fn(2)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.get_by_test_id("All").click()
# 7002-SORD-017
expect(page.get_by_role("row").nth(6)).to_contain_text("10+10LimitFilled120.00GTC")
expect(page.get_by_role("row").nth(6)).to_contain_text(
"10+10LimitFilled120.00GTC"
)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_limit_sell_order(continuous_market, vega: VegaServiceNull, page: Page):
@@ -82,11 +84,13 @@ def test_limit_sell_order(continuous_market, vega: VegaServiceNull, page: Page):
)
page.get_by_test_id(place_order).click()
wait_for_toast_confirmation(page)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.get_by_test_id("All").click()
expect(page.get_by_role("row").nth(7)).to_contain_text("10-10LimitFilled100.00GFN")
expect(page.get_by_role("row").nth(7)).to_contain_text(
"10-10LimitFilled100.00GFN"
)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_market_sell_order(continuous_market, vega: VegaServiceNull, page: Page):
@@ -103,12 +107,14 @@ def test_market_sell_order(continuous_market, vega: VegaServiceNull, page: Page)
)
page.get_by_test_id(place_order).click()
wait_for_toast_confirmation(page)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.get_by_test_id("All").click()
expect(page.get_by_role("row").nth(8)).to_contain_text("10-10MarketFilled-IOC")
expect(page.get_by_role("row").nth(8)).to_contain_text(
"10-10MarketFilled-IOC"
)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_market_buy_order(continuous_market, vega: VegaServiceNull, page: Page):
@@ -118,32 +124,13 @@ def test_market_buy_order(continuous_market, vega: VegaServiceNull, page: Page):
page.get_by_test_id(tif).select_option("Fill or Kill (FOK)")
page.get_by_test_id(place_order).click()
wait_for_toast_confirmation(page)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.get_by_test_id("All").click()
# 7002-SORD-010
# 0003-WTXN-012
# 0003-WTXN-003
expect(page.get_by_role("row").nth(9)).to_contain_text("10+10MarketFilled-FOK")
@pytest.mark.usefixtures("risk_accepted")
def test_sidebar_should_be_open_after_reload(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
expect(page.get_by_test_id("deal-ticket-form")).to_be_visible()
page.get_by_test_id("Order").click()
expect(page.get_by_test_id("deal-ticket-form")).not_to_be_visible()
page.reload()
expect(page.get_by_test_id("deal-ticket-form")).to_be_visible()
@pytest.mark.skip("We currently can't approve wallet connection through Sim")
@pytest.mark.usefixtures("risk_accepted")
def test_connect_vega_wallet(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("order-price").fill("101")
page.get_by_test_id("order-connect-wallet").click()
expect(page.locator('[role="dialog"]')).to_be_visible()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("wallet-dialog-title")).to_be_visible()
# TODO: accept wallet connection and assert wallet is connected.
expect(page.get_by_test_id("order-type-Limit")).to_be_checked()
expect(page.get_by_test_id("order-price")).to_have_value("101")
expect(page.get_by_role("row").nth(9)).to_contain_text(
"10+10MarketFilled-FOK"
)
@@ -0,0 +1,35 @@
import pytest
from playwright.sync_api import Page, expect
from conftest import init_vega
from fixtures.market import setup_continuous_market
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def continuous_market(vega):
return setup_continuous_market(vega)
@pytest.mark.skip("We currently can't approve wallet connection through Sim")
@pytest.mark.usefixtures("risk_accepted")
def test_connect_vega_wallet(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("order-price").fill("101")
page.get_by_test_id("order-connect-wallet").click()
expect(page.locator('[role="dialog"]')).to_be_visible()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("wallet-dialog-title")).to_be_visible()
# TODO: accept wallet connection and assert wallet is connected.
expect(page.get_by_test_id("order-type-Limit")).to_be_checked()
expect(page.get_by_test_id("order-price")).to_have_value("101")
@pytest.mark.usefixtures("risk_accepted")
def test_sidebar_should_be_open_after_reload(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
expect(page.get_by_test_id("deal-ticket-form")).to_be_visible()
page.get_by_test_id("Order").click()
expect(page.get_by_test_id("deal-ticket-form")).not_to_be_visible()
page.reload()
expect(page.get_by_test_id("deal-ticket-form")).to_be_visible()
@@ -41,6 +41,7 @@ close_toast = "toast-close"
def create_position(vega: VegaServiceNull, market_id):
submit_order(vega, "Key 1", market_id, "SIDE_SELL", 100, 110)
submit_order(vega, "Key 1", market_id, "SIDE_BUY", 100, 110)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup
@@ -77,6 +78,7 @@ def test_submit_stop_order_rejected(continuous_market, vega: VegaServiceNull, pa
page.get_by_test_id(trigger_price).fill("103")
page.get_by_test_id(order_size).fill("3")
page.get_by_test_id(submit_stop_order).click()
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.get_by_test_id(close_toast).click()
@@ -267,6 +269,82 @@ class TestStopOcoValidation:
def continuous_market(self, vega):
return setup_continuous_market(vega)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_stop_market_order_form_validation(self, continuous_market, page: Page):
# 7002-SORD-052
# 7002-SORD-055
# 7002-SORD-056
# 7002-SORD-057
# 7002-SORD-058
# 7002-SORD-064
# 7002-SORD-065
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_market_order_btn).is_visible()
page.get_by_test_id(stop_market_order_btn).click()
expect(
page.get_by_test_id("sidebar-content").get_by_text("Trigger").first
).to_be_visible()
expect(page.locator('[for="triggerDirection-risesAbove"]')).to_have_text(
"Rises above"
)
expect(page.locator('[for="triggerDirection-fallsBelow"]')).to_have_text(
"Falls below"
)
page.get_by_test_id(trigger_price).click()
expect(page.get_by_test_id(trigger_price)).to_be_empty
expect(page.locator('[for="triggerType-price"]')).to_have_text("Price")
expect(page.locator('[for="triggerType-trailingPercentOffset"]')).to_have_text(
"Trailing Percent Offset"
)
expect(page.locator('[for="order-size"]')).to_have_text("Size")
page.get_by_test_id(order_size).click()
expect(page.get_by_test_id(order_size)).to_be_empty
expect(page.get_by_test_id(order_price)).not_to_be_visible()
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_stop_limit_order_form_validation(self, continuous_market, page: Page):
# 7002-SORD-020
# 7002-SORD-021
# 7002-SORD-022
# 7002-SORD-033
# 7002-SORD-034
# 7002-SORD-035
# 7002-SORD-036
# 7002-SORD-037
# 7002-SORD-038
# 7002-SORD-049
# 7002-SORD-050
# 7002-SORD-051
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_limit_order_btn).is_visible()
page.get_by_test_id(stop_limit_order_btn).click()
expect(
page.get_by_test_id("sidebar-content").get_by_text("Trigger").first
).to_be_visible()
expect(page.locator('[for="triggerDirection-risesAbove"]')).to_have_text(
"Rises above"
)
expect(page.locator('[for="triggerDirection-risesAbove"]')).to_be_checked
expect(page.locator('[for="triggerDirection-fallsBelow"]')).to_have_text(
"Falls below"
)
page.get_by_test_id(trigger_price).click()
expect(page.get_by_test_id(trigger_price)).to_be_empty
expect(page.locator('[for="triggerType-price"]')).to_have_text("Price")
expect(page.locator('[for="triggerType-price"]')).to_be_checked
expect(page.locator('[for="triggerType-trailingPercentOffset"]')).to_have_text(
"Trailing Percent Offset"
)
expect(page.locator('[for="order-size"]').first).to_have_text("Size")
expect(page.locator('[for="order-price"]').last).to_have_text("Price")
page.get_by_test_id(order_size).click()
expect(page.get_by_test_id(order_size)).to_be_empty
page.get_by_test_id(order_price).click()
expect(page.get_by_test_id(order_price)).to_be_empty()
@pytest.mark.skip("core issue")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_maximum_number_of_active_stop_orders(
+1 -1
View File
@@ -681,4 +681,4 @@ def test_fills_taker_fee_tooltip_discount_program(
row.locator(COL_FEE).hover()
expect(page.get_by_test_id(FEE_BREAKDOWN_TOOLTIP)).to_have_text(
f"If the market was activeFees to be paid by the taker; discounts are already applied.Infrastructure fee{infra_fee} tDAILiquidity fee0.00 tDAIMaker fee{maker_fee} tDAITotal fees{total_fee} tDAI"
)
)
@@ -146,6 +146,29 @@ class TestGetStarted:
# 0007-FUGS-007
expect(page.get_by_test_id("dialog-content").nth(1)).to_be_visible()
def test_browser_wallet_installed(self, simple_market, page: Page):
page.add_init_script("window.vega = {}")
page.goto(f"/#/markets/{simple_market}")
locator = page.get_by_test_id("connect-vega-wallet")
page.wait_for_selector('[data-testid="connect-vega-wallet"]', state="attached")
expect(locator).to_be_enabled
expect(locator).to_be_visible
expect(locator).to_have_text("Connect")
@pytest.mark.usefixtures("risk_accepted")
def test_get_started_deal_ticket(self,simple_market, page: Page):
page.goto(f"/#/markets/{simple_market}")
expect(page.get_by_test_id("order-connect-wallet")).to_have_text("Connect wallet")
@pytest.mark.usefixtures("risk_accepted")
def test_browser_wallet_installed_deal_ticket(simple_market, page: Page):
page.add_init_script("window.vega = {}")
page.goto(f"/#/markets/{simple_market}")
# 0007-FUGS-013
page.wait_for_selector('[data-testid="sidebar-content"]', state="visible")
expect(page.get_by_test_id("get-started-banner")).not_to_be_visible()
@pytest.mark.skip("tbd-market-sim")
def test_redirect_default_market(self, continuous_market, vega: VegaServiceNull, page: Page):
page.goto("/")
@@ -156,3 +179,11 @@ class TestGetStarted:
page.get_by_test_id("icon-cross").click()
# 0007-FUGS-018
expect(page.get_by_test_id("welcome-dialog")).not_to_be_visible()
class TestBrowseAll:
def test_get_started_browse_all(self, simple_market, vega: VegaServiceNull, page: Page):
page.goto("/")
print(simple_market)
page.get_by_test_id("browse-markets-button").click()
# 0007-FUGS-005
expect(page).to_have_url(f"http://localhost:{vega.console_port}/#/markets/{simple_market}")
@@ -35,6 +35,7 @@ class TestIcebergOrdersValidations:
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(page.get_by_test_id("toast-content")).to_have_text(
@@ -50,6 +51,7 @@ def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page
page.goto(f"/#/markets/{continuous_market}")
submit_order(vega, "Key 1", continuous_market, "SIDE_SELL", 102, 101, 2, 1)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -82,6 +84,7 @@ def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page
submit_order(vega, MM_WALLET2.name, continuous_market, "SIDE_BUY", 103, 101)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(
@@ -16,44 +16,34 @@ def vega(request):
def continuous_market(vega):
return setup_continuous_market(vega)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_liquidity_provision_amendment(
continuous_market, vega: VegaServiceNull, page: Page
):
def test_liquidity_provision_amendment(continuous_market, vega: VegaServiceNull, page: Page):
# TODO Refactor asserting the grid
page.goto(f"/#/liquidity/{continuous_market}")
change_keys(page, vega, "market_maker")
row = (
page.get_by_test_id("tab-myLP")
.locator(".ag-center-cols-container .ag-row")
.first
row = page.get_by_test_id(
"tab-myLP").locator(".ag-center-cols-container .ag-row").first
expect(row).to_contain_text(
"Active"
)
expect(row).to_contain_text("Active")
# 5002-LIQP-006
expect(page.get_by_test_id("target-stake")).to_have_text("Target stake5.82757 tDAI")
expect(page.get_by_test_id("target-stake")
).to_have_text("Target stake5.82757 tDAI")
# 5002-LIQP-007
expect(page.get_by_test_id("supplied-stake")).to_have_text(
"Supplied stake10,000.00 tDAI"
)
expect(page.get_by_test_id("supplied-stake")
).to_have_text("Supplied stake10,000.00 tDAI")
# 5002-LIQP-008
expect(page.get_by_test_id("liquidity-supplied")).to_have_text(
"Liquidity supplied 171,598.11%"
)
expect(page.get_by_test_id("liquidity-supplied")
).to_have_text("Liquidity supplied 171,598.11%")
expect(page.get_by_test_id("fees-paid")).to_have_text("Fees paid-")
# 5002-LIQP-009
expect(page.get_by_test_id("liquidity-market-id")).to_have_text(
"Market ID" + truncate_middle(continuous_market)
)
expect(page.get_by_test_id("liquidity-learn-more")).to_have_text(
"Learn moreProviding liquidity"
)
expect(page.get_by_test_id("liquidity-market-id")
).to_have_text("Market ID" + truncate_middle(continuous_market))
expect(page.get_by_test_id("liquidity-learn-more")
).to_have_text("Learn moreProviding liquidity")
# 002-LIQP-010
expect(
page.get_by_test_id("liquidity-learn-more").get_by_test_id("external-link")
).to_have_attribute(
"href", "https://docs.vega.xyz/testnet/concepts/liquidity/provision"
)
expect(page.get_by_test_id("liquidity-learn-more").get_by_test_id("external-link")
).to_have_attribute("href", "https://docs.vega.xyz/testnet/concepts/liquidity/provision")
vega.submit_simple_liquidity(
key_name="market_maker",
@@ -66,50 +56,41 @@ def test_liquidity_provision_amendment(
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.reload()
row = (
page.get_by_test_id("tab-myLP")
.locator(".ag-center-cols-container .ag-row")
.first
row = page.get_by_test_id(
"tab-myLP").locator(".ag-center-cols-container .ag-row").first
expect(row).to_contain_text(
"Updating next epoch"
)
expect(row).to_contain_text("Updating next epoch")
next_epoch(vega=vega)
page.reload()
expect(page.get_by_test_id("supplied-stake")).to_have_text(
"Supplied stake1.00001 tDAI"
expect(page.get_by_test_id("supplied-stake")
).to_have_text("Supplied stake1.00001 tDAI")
expect(page.get_by_test_id("liquidity-supplied")
).to_have_text("Liquidity supplied 17.16%")
row = page.get_by_test_id(
"tab-myLP").locator(".ag-center-cols-container .ag-row").first
expect(row).to_contain_text(
"Active"
)
expect(page.get_by_test_id("liquidity-supplied")).to_have_text(
"Liquidity supplied 17.16%"
)
row = (
page.get_by_test_id("tab-myLP")
.locator(".ag-center-cols-container .ag-row")
.first
)
expect(row).to_contain_text("Active")
@pytest.mark.skip("Waiting for the ability to cancel LP")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_liquidity_provision_cancelled(
continuous_market, vega: VegaServiceNull, page: Page
):
def test_liquidity_provision_inactive(continuous_market, vega: VegaServiceNull, page: Page):
# TODO Refactor asserting the grid
page.goto(f"/#/liquidity/{continuous_market}")
change_keys(page, vega, "market_maker")
row = (
page.get_by_test_id("tab-myLP")
.locator(".ag-center-cols-container .ag-row")
.first
row = page.get_by_test_id(
"tab-myLP").locator(".ag-center-cols-container .ag-row").first
expect(row).to_contain_text(
"Active"
)
expect(row).to_contain_text("Active")
vega.cancel_liquidity(
vega.submit_simple_liquidity(
key_name="market_maker",
market_id=continuous_market,
commitment_amount=0,
fee=0,
is_amendment=False,
)
next_epoch(vega=vega)
page.reload()
expect(page.get_by_test_id("supplied-stake")).to_have_text(
"Supplied stake0.00 tDAI"
)
expect(page.get_by_test_id("liquidity-supplied")).to_have_text(
"Liquidity supplied 0.00%"
)
expect(page.locator(".ag-overlay-panel")).to_have_text("No data")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -5,7 +5,6 @@ from vega_sim.null_service import VegaServiceNull
from playwright.sync_api import Page, expect
from fixtures.market import setup_continuous_market
from conftest import init_vega
from actions.utils import next_epoch
@pytest.fixture(scope="class")
@@ -22,7 +21,9 @@ def create_settled_market(vega: VegaServiceNull):
settlement_price=110,
market_id=market_id,
)
next_epoch(vega=vega)
vega.forward("10s")
vega.wait_fn(10)
vega.wait_for_total_catchup()
class TestSettledMarket:
@@ -122,7 +123,9 @@ def test_terminated_market_no_settlement_date(page: Page, vega: VegaServiceNull)
payload={"trading.terminated": "true"},
key_name="FJMKnwfZdd48C8NqvYrG",
)
next_epoch(vega=vega)
vega.forward("60s")
vega.wait_fn(10)
vega.wait_for_total_catchup()
page.goto(f"/#/markets/all")
page.get_by_test_id("Closed markets").click()
row_selector = page.locator(
@@ -0,0 +1,222 @@
import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order
from actions.utils import change_keys
from wallet_config import MM_WALLET, MM_WALLET2
import logging
logger = logging.getLogger()
table_row_selector = (
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row'
)
trading_mode_col = '[col-id="tradingMode"]'
state_col = '[col-id="state"]'
item_value = "item-value"
price_monitoring_bounds_row = "key-value-table-row"
market_trading_mode = "market-trading-mode"
market_state = "market-state"
liquidity_supplied = "liquidity-supplied"
item_value = "item-value"
price_monitoring_bounds_row = "key-value-table-row"
market_trading_mode = "market-trading-mode"
market_state = "market-state"
liquidity_supplied = "liquidity-supplied"
initial_commitment: float = 100
initial_price: float = 1
initial_volume: float = 1
initial_spread: float = 0.1
market_name = "BTC:DAI_2023"
@pytest.mark.skip("tbd")
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_price_monitoring(simple_market, vega: VegaServiceNull, page: Page):
page.goto(f"/#/markets/all")
expect(page.locator(table_row_selector).locator(trading_mode_col)).to_have_text(
"Opening auction"
)
expect(page.locator(table_row_selector).locator('[col-id="state"]')).to_have_text(
"Pending"
)
result = page.get_by_text(market_name)
result.first.click()
page.get_by_test_id(market_trading_mode).get_by_text("Opening auction").hover()
expect(page.get_by_test_id("opening-auction-sub-status").first).to_have_text(
"Opening auction: Not enough liquidity to open"
)
logger.info(page.get_by_test_id("opening-auction-sub-status").inner_text)
vega.submit_liquidity(
key_name=MM_WALLET.name,
market_id=simple_market,
commitment_amount=initial_commitment,
fee=0.002,
is_amendment=False,
)
vega.submit_order(
market_id=simple_market,
trading_key=MM_WALLET.name,
side="SIDE_BUY",
order_type="TYPE_LIMIT",
price=initial_price - 0.0005,
wait=False,
time_in_force="TIME_IN_FORCE_GTC",
volume=99,
)
vega.submit_order(
market_id=simple_market,
trading_key=MM_WALLET.name,
side="SIDE_SELL",
order_type="TYPE_LIMIT",
price=initial_price + 0.0005,
wait=False,
time_in_force="TIME_IN_FORCE_GTC",
volume=99,
)
# 6002-MDET-009
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("0.00 (0.00%)")
# add orders to provide liquidity
submit_order(
vega, MM_WALLET.name, simple_market, "SIDE_BUY", initial_volume, initial_price
)
submit_order(
vega, MM_WALLET.name, simple_market, "SIDE_SELL", initial_volume, initial_price
)
submit_order(
vega,
MM_WALLET.name,
simple_market,
"SIDE_BUY",
initial_volume,
initial_price + initial_spread / 2,
)
submit_order(
vega,
MM_WALLET.name,
simple_market,
"SIDE_SELL",
initial_volume,
initial_price + initial_spread / 2,
)
submit_order(
vega, MM_WALLET2.name, simple_market, "SIDE_SELL", initial_volume, initial_price
)
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("100.00 (>100%)")
vega.forward("10s")
vega.wait_fn(10)
vega.wait_for_total_catchup()
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("50.00 (>100%)")
page.goto(f"/#/markets/all")
# temporary skip
# expect(page.locator(table_row_selector).locator(trading_mode_col)).to_have_text(
# "Continuous"
# )
# commented out because we have an issue #4233
# expect(page.locator(row_selector).locator(state_col)
# ).to_have_text("Pending")
page.goto(f"/#/markets/all")
result = page.get_by_text(market_name)
result.first.click()
page.get_by_test_id("Info").click()
page.get_by_test_id("accordion-title").get_by_text(
"Price monitoring bounds 1"
).click()
expect(
page.get_by_test_id(price_monitoring_bounds_row).first.get_by_text(
"1.32217 BTC"
)
).to_be_visible()
expect(
page.get_by_test_id(price_monitoring_bounds_row).last.get_by_text("0.79245 BTC")
).to_be_visible()
# add orders that change the price so that it goes beyond the limits of price monitoring
submit_order(vega, MM_WALLET.name, simple_market, "SIDE_SELL", 100, 110)
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_BUY", 100, 90)
submit_order(vega, MM_WALLET.name, simple_market, "SIDE_SELL", 100, 105)
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_BUY", 100, 95)
# add order at the current price so that it is possible to change the status to price monitoring
to_cancel = submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_BUY", 1, 105)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(
page.get_by_test_id(price_monitoring_bounds_row).first.get_by_text(
"135.44204 BTC"
)
).to_be_visible()
expect(
page.get_by_test_id(price_monitoring_bounds_row).last.get_by_text(
"81.17758 BTC"
)
).to_be_visible()
expect(
page.get_by_test_id(market_trading_mode).get_by_test_id(item_value)
).to_have_text("Monitoring auction - price")
expect(page.get_by_test_id(market_state).get_by_test_id(item_value)).to_have_text(
"Suspended"
)
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("50.00 (8.78%)")
# cancel order to increase liquidity
vega.cancel_order(MM_WALLET2.name, simple_market, to_cancel)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(page.get_by_text(market_name).first).to_be_attached()
expect(
page.get_by_test_id(market_trading_mode).get_by_test_id(item_value)
).to_have_text("Continuous")
expect(page.get_by_test_id(market_state).get_by_test_id(item_value)).to_have_text(
"Active"
)
# commented out because we have an issue #4233
# expect(page.get_by_text("Opening auction")).to_be_hidden()
# 6002-MDET-009
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("50.00 (>100%)")
COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_auction_uncross_fees(continuous_market, vega: VegaServiceNull, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("Fills").click()
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
page.get_by_role("gridcell", name="0.00 tDAI").nth(0).hover()
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text(
"If the market was suspendedDuring auction, half the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI"
)
change_keys(page, vega, "market_maker")
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
page.get_by_role("gridcell", name="0.00 tDAI").nth(0).hover()
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text(
"If the market was suspendedDuring auction, half the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI"
)
@@ -25,3 +25,65 @@ def test_market_selector(continuous_market, page: Page):
expect(btc_market.locator("span.rounded-md.leading-none")).to_be_visible()
expect(btc_market.locator("span.rounded-md.leading-none")).to_have_text("Futr")
expect(btc_market.locator('[data-testid="sparkline-svg"]')).not_to_be_visible
@pytest.mark.usefixtures("simple_market", "auth", "risk_accepted")
@pytest.mark.parametrize(
"simple_market",
[
{
"custom_market_name": "APPL.MF21",
"custom_asset_name": "tUSDC",
"custom_asset_symbol": "tUSDC",
}
],
indirect=True,
)
def test_market_selector_filter(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("header-title").click()
# 6001-MARK-027
page.get_by_test_id("product-Spot").click()
expect(page.get_by_test_id("market-selector-list")).to_contain_text(
"Spot markets coming soon."
)
page.get_by_test_id("product-Perpetual").click()
expect(page.get_by_test_id("market-selector-list")).to_contain_text(
"No perpetual markets."
)
page.get_by_test_id("product-Future").click()
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(2)
# 6001-MARK-029
page.get_by_test_id("search-term").fill("btc")
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(1)
# tbd - 5465
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_contain_text(
"BTC:DAI_2023107.50 tDAI"
)
page.get_by_test_id("search-term").clear()
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(2)
# 6001-MARK-030
# 6001-MARK-031
# 6001-MARK-032
# 6001-MARK-033
page.get_by_test_id("sort-trigger").click()
expect(page.get_by_test_id("sort-item-Gained")).to_have_text("Top gaining")
expect(page.get_by_test_id("sort-item-Gained")).to_be_visible()
expect(page.get_by_test_id("sort-item-Lost")).to_have_text("Top losing")
expect(page.get_by_test_id("sort-item-Lost")).to_be_visible()
expect(page.get_by_test_id("sort-item-New")).to_have_text("New markets")
expect(page.get_by_test_id("sort-item-New")).to_be_visible()
# 6001-MARK-028
page.get_by_test_id("sort-trigger").click(force=True)
page.get_by_test_id("asset-trigger").click()
page.get_by_role("menuitemcheckbox").nth(0).get_by_text("tDAI").click()
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(1)
# tbd - 5465
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_contain_text(
"BTC:DAI_2023107.50 tDAI"
)
@@ -0,0 +1,35 @@
import pytest
from playwright.sync_api import Page, expect
from conftest import init_page, init_vega, risk_accepted_setup
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def page(vega, browser, request):
with init_page(vega, browser, request) as page:
risk_accepted_setup(page)
page.goto("/#/markets/all")
yield page
def test_no_open_markets(page: Page):
# 6001-MARK-034
page.get_by_test_id("Open markets").click()
expect(page.locator(".ag-overlay-wrapper")).to_have_text("No markets")
def test_no_closed_markets(page: Page):
page.get_by_test_id("Closed markets").click()
expect(page.locator(".ag-overlay-wrapper")).to_have_text("No markets")
def test_no_proposed_markets(page: Page):
# 6001-MARK-061
page.get_by_test_id("Proposed markets").click()
expect(page.locator(".ag-overlay-wrapper")).to_have_text("No proposed markets")
@@ -4,14 +4,9 @@ from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order
from fixtures.market import setup_simple_market
from conftest import init_vega
from actions.utils import wait_for_toast_confirmation, change_keys
from actions.utils import wait_for_toast_confirmation
from wallet_config import MM_WALLET, MM_WALLET2
market_trading_mode = "market-trading-mode"
market_state = "market-state"
item_value = "item-value"
COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
@pytest.fixture(scope="module")
def vega(request):
@@ -138,29 +133,3 @@ def test_market_monitoring_auction_price_volatility_market_order(
"This market is in auction due to high price volatility. Only limit orders are permitted when market is in auction."
)
expect(page.get_by_test_id("deal-ticket-error-message-type")).to_be_visible()
@pytest.mark.usefixtures("risk_accepted", "auth", "setup_market_monitoring_auction")
def test_market_price_volatility(
page: Page, simple_market, vega: VegaServiceNull
):
page.goto(f"/#/markets/{simple_market}")
expect(
page.get_by_test_id(market_trading_mode).get_by_test_id(item_value)
).to_have_text("Monitoring auction - price")
expect(page.get_by_test_id(market_state).get_by_test_id(item_value)).to_have_text(
"Suspended"
)
@pytest.mark.usefixtures("risk_accepted", "auth", "setup_market_monitoring_auction")
def test_auction_uncross_fees(simple_market, vega: VegaServiceNull, page: Page):
page.goto(f"/#/markets/{simple_market}")
change_keys(page, vega, "market_maker")
page.get_by_test_id("Fills").click()
row = page.locator('[row-index="3"]').nth(1)
expect(row.locator('[col-id="fee"]')).to_have_text("0.00 tDAI")
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
row.locator('[col-id="fee"]').hover()
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text(
"If the market was suspendedDuring auction, half the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI"
)
@@ -73,6 +73,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
)
# "wait" for market to be approved and enacted
vega.forward("60s")
vega.wait_fn(10)
vega.wait_for_total_catchup()
next_epoch(vega=vega)
# check that market is in pending state
expect(trading_mode).to_have_text("Opening auction")
@@ -115,7 +118,8 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
submit_order(vega, MM_WALLET.name, market_id, "SIDE_SELL", 1, 100)
submit_order(vega, MM_WALLET2.name, market_id, "SIDE_BUY", 1, 100)
vega.wait_fn(2)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
# check market state is now active and trading mode is continuous
@@ -135,7 +139,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
.get_by_test_id(f"update-state-banner-{market_id}")
).to_be_visible()
next_epoch(vega=vega)
vega.forward("60s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(
page.get_by_test_id("market-banner")
@@ -149,7 +155,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
forward_time_to_enactment = False
)
next_epoch(vega=vega)
vega.forward("60s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(page.get_by_test_id("market-banner")).not_to_be_visible()
@@ -162,7 +170,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
payload={"trading.terminated": "true"},
key_name=GOVERNANCE_WALLET.name,
)
next_epoch(vega=vega)
vega.forward("60s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
# market state should be changed to "No trading" because of the invalid oracle
expect(trading_mode).to_have_text("No trading")
@@ -174,7 +184,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
settlement_price=100,
market_id=market_id,
)
next_epoch(vega=vega)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
# check market state is now settled
expect(trading_mode).to_have_text("No trading")
@@ -44,7 +44,7 @@ def verify_order_value(
actual_text = element.text_content()
if actual_text is None:
raise Exception(f"no text found for test_id {test_id}")
raise Exception(f"no text found for test_id {test_id}")
assert re.match(
expected_text, actual_text
@@ -144,4 +144,5 @@ def test_limit_order_trade_order_trade_away(continuous_market, page: Page):
page.get_by_test_id("Orderbook").click()
price_element = page.get_by_test_id("price-11000000").nth(1)
# 6003-ORDB-010
print(price_element)
expect(price_element).to_be_hidden()
@@ -106,6 +106,7 @@ def test_orderbook_grid_content(setup_market, page: Page):
matching_order[1],
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -232,6 +233,7 @@ def test_orderbook_price_movement(setup_market, page: Page):
matching_order_1[1],
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -252,6 +254,7 @@ def test_orderbook_price_movement(setup_market, page: Page):
matching_order_2[1],
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -34,6 +34,7 @@ class TestPerpetuals:
settlement_price=110,
market_id=perps_market,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
submit_multiple_orders(
@@ -47,7 +48,8 @@ class TestPerpetuals:
settlement_price=110,
market_id=perps_market,
)
vega.wait_fn(10)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
return perps_market
@@ -108,6 +110,7 @@ def test_perps_market_termination_proposed(page: Page, vega: VegaServiceNull):
forward_time_to_enactment=False,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
banner_text = page.get_by_test_id(
@@ -132,6 +135,7 @@ def test_perps_market_terminated(page: Page, vega: VegaServiceNull):
approve_proposal=True,
forward_time_to_enactment=True,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
+1 -1
View File
@@ -11,7 +11,7 @@ def check_pnl_color_value(element, expected_color, expected_value):
assert color == expected_color, f"Unexpected color: {color}"
assert value == expected_value, f"Unexpected value: {value}"
#TODO move this test to jest
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_pnl(continuous_market, vega: VegaServiceNull, page: Page):
page.set_viewport_size({"width": 1748, "height": 977})
@@ -0,0 +1,32 @@
import os
import pytest
from playwright.sync_api import Page, expect
#TODO migrate to jest
@pytest.mark.usefixtures("auth", "risk_accepted", "continuous_market")
def test_ledger_entries_downloads(page: Page):
page.goto("/#/portfolio")
page.get_by_test_id("Ledger entries").click()
expect(page.get_by_test_id("ledger-download-button")).to_be_enabled()
# 7007-LEEN-001
page.get_by_test_id("ledger-download-button").click()
# 7007-LEEN-009
expect(page.get_by_test_id("toast-content")).to_contain_text(("Your file is ready"))
# Get the user's Downloads directory
downloads_directory = os.path.expanduser("~") + "/Downloads/"
# Start waiting for the download
with page.expect_download() as download_info:
# Perform the action that initiates download
page.get_by_role("link", name="Get file here").click()
download = download_info.value
# Wait for the download process to complete and save the downloaded file in the Downloads directory
download.save_as(os.path.join(downloads_directory, download.suggested_filename))
# Verify the download by asserting that the file exists
downloaded_file_path = os.path.join(
downloads_directory, download.suggested_filename
)
assert os.path.exists(
downloaded_file_path
), f"Download failed! File not found at: {downloaded_file_path}"
@@ -15,6 +15,7 @@ def test_closed_market_position(vega: VegaServiceNull, page: Page):
settlement_price=110,
market_id=market_id,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.goto(f"/#/markets/{market_id}")
@@ -1,67 +0,0 @@
import pytest
import vega_sim.proto.vega as vega_protos
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.utils import next_epoch
from wallet_config import MM_WALLET, PARTY_A, PARTY_B
from vega_sim.service import MarketStateUpdateType
import vega_sim.api.governance as governance
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.update_network_parameter(
MM_WALLET.name, parameter="reward.asset", new_value=tDAI_asset_id
)
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_A.name, asset=tDAI_asset_id, amount=100000)
next_epoch(vega=vega)
vega.recurring_transfer(
from_key_name=PARTY_A.name,
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
asset=tDAI_asset_id,
reference="reward",
markets=[continuous_market],
asset_for_metric=tDAI_asset_id,
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
lock_period=5,
amount=100,
factor=1.0,
)
vega.submit_order(
trading_key=PARTY_B.name,
market_id=continuous_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key=PARTY_A.name,
market_id=continuous_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
next_epoch(vega=vega)
vega.update_market_state(
market_id=continuous_market,
proposal_key=MM_WALLET.name,
market_state=MarketStateUpdateType.Suspend,
forward_time_to_enactment=True,
)
next_epoch(vega=vega)
page.goto("/#/rewards")
expect(page.locator(".from-vega-cdark-400")).to_be_visible()
governance.submit_oracle_data(
wallet=vega.wallet,
payload={"trading.terminated": "true"},
key_name="FJMKnwfZdd48C8NqvYrG",
)
next_epoch(vega=vega)
page.reload()
expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()
@@ -0,0 +1,36 @@
import pytest
from playwright.sync_api import expect, Page
settings_icon = "icon-cog"
settings_column_btn = "popover-trigger"
settings_close_btn = "settings-close"
split_view_view = "split-view-view"
@pytest.mark.usefixtures("page", "continuous_market", "auth", "risk_accepted")
def test_column_settings_is_visible(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
expect(page.get_by_test_id(split_view_view).get_by_test_id(settings_column_btn)).to_be_visible()
page.goto("/#/portfolio")
expect(page.get_by_test_id(split_view_view).get_by_test_id(settings_column_btn).nth(0)).to_be_visible()
expect(page.get_by_test_id(split_view_view).get_by_test_id(settings_column_btn).nth(1)).to_be_visible()
page.goto(f"/#/markets/all")
expect(page.get_by_test_id(settings_column_btn)).to_be_visible()
page.click('[data-testid="Proposed markets"]')
expect(page.get_by_test_id(settings_column_btn)).to_be_visible()
page.click('[data-testid="Closed markets"]')
expect(page.get_by_test_id(settings_column_btn)).to_be_visible()
@pytest.mark.usefixtures("page", "continuous_market", "auth", "risk_accepted")
def test_can_reset_columns_state(continuous_market, page: Page):
page.goto(f"/#/markets/all")
col_market = page.locator('[col-id="tradableInstrument.instrument.code"]').first
col_settlement_asset = page.locator('[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"]').first
col_market.drag_to(col_settlement_asset)
# Check the attribute of the dragged element
attribute_value = col_market.get_attribute("aria-colindex")
assert attribute_value != "1"
page.get_by_test_id(settings_column_btn).click()
page.get_by_role("button", name="Reset Columns").click()
attribute_value_after_reset = col_market.get_attribute("aria-colindex")
assert attribute_value_after_reset == "1"
@@ -149,5 +149,7 @@ def provide_successor_liquidity(
)
submit_order(vega, "Key 1", market_id, "SIDE_BUY", 1, 110)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -0,0 +1,78 @@
import pytest
import re
import logging
from playwright.sync_api import expect
from actions.vega import submit_order
from conftest import init_vega
from playwright.sync_api import Page
from vega_sim.null_service import VegaServiceNull
logger = logging.getLogger()
@pytest.fixture(scope="module")
def vega():
with init_vega() as vega:
yield vega
# Could be turned into a helper function in the future.
def verify_data_grid(page: Page, data_test_id, expected_pattern):
page.get_by_test_id(data_test_id).click()
# Required so that we can get liquidation price
expect(
page.locator(
f'[data-testid^="tab-{data_test_id.lower()}"] >> .ag-center-cols-container .ag-row-first'
)
).to_be_visible()
actual_text = page.locator(
f'[data-testid^="tab-{data_test_id.lower()}"] >> .ag-center-cols-container'
).text_content()
lines = actual_text.strip().split("\n")
for expected, actual in zip(expected_pattern, lines):
# We are using regex so that we can run tests in different timezones.
if re.match(r"^\\d", expected): # check if it's a regex
if re.search(expected, actual):
logger.info(f"Matched: {expected} == {actual}")
else:
logger.info(f"Not Matched: {expected} != {actual}")
raise AssertionError(f"Pattern does not match: {expected} != {actual}")
else: # it's not a regex, so we escape it
if re.search(re.escape(expected), actual):
logger.info(f"Matched: {expected} == {actual}")
else:
logger.info(f"Not Matched: {expected} != {actual}")
raise AssertionError(f"Pattern does not match: {expected} != {actual}")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_limit_order_new_trade_top_of_list(
continuous_market, vega: VegaServiceNull, page: Page
):
submit_order(vega, "Key 1", continuous_market, "SIDE_BUY", 1, 110)
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.goto(f"/#/markets/{continuous_market}")
expected_trade = [
"103.50",
"1",
r"\d{1,2}/\d{1,2}/\d{4},\s*\d{1,2}:\d{2}:\d{2}\s*(?:AM|PM)" "107.50",
"1",
r"\d{1,2}/\d{1,2}/\d{4},\s*\d{1,2}:\d{2}:\d{2}\s*(?:AM|PM)",
]
# 6005-THIS-001
# 6005-THIS-002
# 6005-THIS-003
# 6005-THIS-004
# 6005-THIS-005
# 6005-THIS-006
verify_data_grid(page, "Trades", expected_trade)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_price_copied_to_deal_ticket(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("Trades").click()
page.locator("[col-id=price]").nth(1).click()
# 6005-THIS-007
expect(page.get_by_test_id("order-price")).to_have_value("107.50000")
@@ -22,6 +22,8 @@ def test_trade_match_table(opening_auction_market: str, vega: VegaServiceNull, p
price=10e15,
wait=False,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -32,6 +34,7 @@ def test_trade_match_table(opening_auction_market: str, vega: VegaServiceNull, p
"SIDE_BUY",
[[5, 110], [5, 105], [1, 50]],
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -42,6 +45,7 @@ def test_trade_match_table(opening_auction_market: str, vega: VegaServiceNull, p
"SIDE_SELL",
[[5, 90], [5, 95], [1, 150]],
)
vega.forward("60s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -50,6 +50,7 @@ def test_transfer_submit(continuous_market, vega: VegaServiceNull, page: Page):
page.locator('[data-testid=transfer-form] [type="submit"]').click()
wait_for_toast_confirmation(page)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expected_confirmation_text = re.compile(
@@ -141,12 +142,14 @@ def test_transfer_vesting_below_minimum(
asset=asset_id,
amount=24.999999,
)
vega.forward("10s")
vega.wait_fn(10)
vega.wait_for_total_catchup()
page.get_by_test_id("use-max-button").first.click()
page.locator('[data-testid=transfer-form] [type="submit"]').click()
wait_for_toast_confirmation(page)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expected_confirmation_text = re.compile(
@@ -0,0 +1,133 @@
import pytest
import re
import json
from playwright.sync_api import Page, expect, Route
from conftest import init_vega
from fixtures.market import setup_continuous_market
order_size = "order-size"
order_price = "order-price"
place_order = "place-order"
order_side_sell = "order-side-SIDE_SELL"
market_order = "order-type-Market"
tif = "order-tif"
expire = "expire"
api_request_match = r"http://localhost:\d+/api/v2/requests"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def continuous_market(vega):
return setup_continuous_market(vega)
def handle_route_connection_lost(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
route.fulfill(
status=200,
headers={"Content-Type": "application/json"},
body='{"jsonrpc": "2.0", "id": "1"}',
)
else:
route.continue_()
def handle_route_connection_rejected(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
custom_response = {
"jsonrpc": "2.0",
"error": {
"code": 3001,
"data": "the user rejected the wallet connection",
"message": "User error",
},
"id": "0",
}
route.fulfill(
status=400,
headers={"Content-Type": "application/json"},
body=json.dumps(custom_response),
)
else:
route.continue_()
def assert_connection_approve(route: Route, request, page: Page):
if request.method == "POST" and re.match(api_request_match, request.url):
expect(page.get_by_test_id("toast-content")).to_have_text(
"Please go to your Vega wallet application and approve or reject the transaction."
)
else:
route.continue_()
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_wallet_connection_error(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("wallet-dialog-title")).to_have_text(
"Something went wrong"
)
@pytest.mark.usefixtures("risk_accepted")
def test_wallet_connection_rejected(continuous_market, page: Page):
# 0002-WCON-002
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-015
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("dialog-content").nth(1)).to_have_text(
"User errorthe user rejected the wallet connectionTry againAbout the Vega wallet | Supported browsers "
)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_wallet_connection_error_transaction(continuous_market, page: Page):
# 0003-WTXN-009
# 0003-WTXN-011
# 0002-WCON-016
# 0003-WTXN-008
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content")).to_have_text(
"Wallet disconnectedThe connection to your Vega Wallet has been lost.Connect vega wallet"
)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_wallet_transaction_rejected(continuous_market, page: Page):
# 0003-WTXN-007
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content").nth(0)).to_have_text(
"Error occurredthe user rejected the wallet connection"
)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_wallet_connection_approve(continuous_market, page: Page):
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-009
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", assert_connection_approve)
page.get_by_test_id(place_order).click()
@@ -0,0 +1,62 @@
import {
Intent,
useToasts,
ToastHeading,
CLOSE_AFTER,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEffect, useMemo } from 'react';
import { useT } from '../use-t';
import { VegaWalletConnectButton } from '../../components/vega-wallet-connect-button';
const WALLET_DISCONNECTED_TOAST_ID = 'WALLET_DISCONNECTED_TOAST_ID';
export const useWalletDisconnectedToasts = () => {
const t = useT();
const [hasToast, setToast, updateToast] = useToasts((state) => [
state.hasToast,
state.setToast,
state.update,
]);
const { isAlive } = useVegaWallet();
const toast = useMemo(
() => ({
id: WALLET_DISCONNECTED_TOAST_ID,
intent: Intent.Danger,
content: (
<>
<ToastHeading>{t('Wallet connection lost')}</ToastHeading>
<p>{t('The connection to the Vega wallet has been lost.')}</p>
<p className="mt-2">
<VegaWalletConnectButton
intent={Intent.Danger}
onClick={() => {
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
hidden: true,
});
}}
/>
</p>
</>
),
onClose: () => {
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
hidden: true,
});
},
closeAfter: CLOSE_AFTER,
}),
[t, updateToast]
);
useEffect(() => {
if (isAlive === false) {
if (hasToast(WALLET_DISCONNECTED_TOAST_ID)) {
updateToast(WALLET_DISCONNECTED_TOAST_ID, { hidden: false });
} else {
setToast(toast);
}
}
}, [hasToast, isAlive, setToast, t, toast, updateToast]);
};
+3 -22
View File
@@ -1,4 +1,4 @@
import { Intent, ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { useProposalToasts } from '@vegaprotocol/proposals';
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
@@ -6,26 +6,7 @@ import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
import { Links } from '../lib/links';
import { useReferralToasts } from '../client-pages/referrals/hooks/use-referral-toasts';
import {
useWalletDisconnectToastActions,
useWalletDisconnectedToasts,
} from '@vegaprotocol/web3';
import { VegaWalletConnectButton } from '../components/vega-wallet-connect-button';
const WalletDisconnectAdditionalContent = () => {
const { hideToast } = useWalletDisconnectToastActions();
return (
<p className="mt-2">
<VegaWalletConnectButton
intent={Intent.Danger}
onClick={() => {
// hide toast when clicked on `Connect`
hideToast();
}}
/>
</p>
);
};
import { useWalletDisconnectedToasts } from '../lib/hooks/use-wallet-disconnected-toasts';
export const ToastsManager = () => {
useProposalToasts();
@@ -36,7 +17,7 @@ export const ToastsManager = () => {
withdrawalsLink: Links.PORTFOLIO(),
});
useReferralToasts();
useWalletDisconnectedToasts(<WalletDisconnectAdditionalContent />);
useWalletDisconnectedToasts();
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
+86 -1
View File
@@ -96,6 +96,11 @@ describe('TransferForm', () => {
});
it.each([
{
targetText: 'Include transfer fee',
tooltipText:
'The fee will be taken from the amount you are transferring.',
},
{
targetText: 'Transfer fee',
tooltipText: /transfer\.fee\.factor/,
@@ -271,6 +276,9 @@ describe('TransferForm', () => {
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
await userEvent.clear(amountInput);
await userEvent.type(amountInput, '50');
@@ -280,7 +288,10 @@ describe('TransferForm', () => {
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue('100.00');
// If transfering from a vested account fees should be 0
// If transfering from a vested account 'include fees' checkbox should
// be disabled and fees should be 0
expect(checkbox).not.toBeChecked();
expect(checkbox).toBeDisabled();
const expectedFee = '0';
const total = new BigNumber(amount).plus(expectedFee).toFixed();
@@ -385,7 +396,78 @@ describe('TransferForm', () => {
});
});
});
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
const mockSubmit = jest.fn();
renderComponent({ ...props, submitTransfer: mockSubmit });
// check current pubkey not shown
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
pubKeyOptions
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1]
);
// Select asset
await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
// 1003-TRAN-022
expect(checkbox).not.toBeChecked();
await userEvent.clear(amountInput);
await userEvent.type(amountInput, amount);
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
// 1003-TRAN-020
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
expectedAmount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
amount
);
await submit();
await waitFor(() => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(expectedAmount, asset.decimals),
oneOff: {},
});
});
});
it('validates fields when checkbox is not checked', async () => {
renderComponent(props);
@@ -415,8 +497,11 @@ describe('TransferForm', () => {
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
await userEvent.type(amountInput, amount);
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
+54 -12
View File
@@ -15,6 +15,7 @@ import {
TradingRichSelect,
TradingSelect,
Tooltip,
TradingCheckbox,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
@@ -134,17 +135,32 @@ export const TransferForm = ({
const accountBalance =
account && addDecimal(account.balance, account.asset.decimals);
const [includeFee, setIncludeFee] = useState(false);
// Max amount given selected asset and from account
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
const fee = useMemo(
() => feeFactor && new BigNumber(feeFactor).times(amount).toString(),
[amount, feeFactor]
);
const transferAmount = useMemo(() => {
if (!amount) return undefined;
if (includeFee && feeFactor) {
return new BigNumber(1).minus(feeFactor).times(amount).toString();
}
return amount;
}, [amount, includeFee, feeFactor]);
const fee = useMemo(() => {
if (!transferAmount) return undefined;
if (includeFee) {
return new BigNumber(amount).minus(transferAmount).toString();
}
return (
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
);
}, [amount, includeFee, transferAmount, feeFactor]);
const onSubmit = useCallback(
(fields: FormFields) => {
if (!amount) {
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
@@ -157,7 +173,7 @@ export const TransferForm = ({
const transfer = normalizeTransfer(
fields.toVegaKey,
amount,
transferAmount,
type,
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
{
@@ -167,7 +183,7 @@ export const TransferForm = ({
);
submitTransfer(transfer);
},
[submitTransfer, amount, assets]
[submitTransfer, transferAmount, assets]
);
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
@@ -263,6 +279,7 @@ export const TransferForm = ({
) {
setValue('toVegaKey', pubKey);
setToVegaKeyMode('select');
setIncludeFee(false);
}
}}
>
@@ -432,9 +449,27 @@ export const TransferForm = ({
</TradingInputError>
)}
</TradingFormGroup>
{amount && fee && (
<div className="mb-4">
<Tooltip
description={t(
`The fee will be taken from the amount you are transferring.`
)}
>
<div>
<TradingCheckbox
name="include-transfer-fee"
disabled={!transferAmount || fromVested}
label={t('Include transfer fee')}
checked={includeFee}
onCheckedChange={() => setIncludeFee((x) => !x)}
/>
</div>
</Tooltip>
</div>
{transferAmount && fee && (
<TransferFee
amount={amount}
amount={transferAmount}
transferAmount={transferAmount}
feeFactor={feeFactor}
fee={fromVested ? '0' : fee}
decimals={asset?.decimals}
@@ -449,22 +484,29 @@ export const TransferForm = ({
export const TransferFee = ({
amount,
transferAmount,
feeFactor,
fee,
decimals,
}: {
amount: string;
transferAmount: string;
feeFactor: string | null;
fee?: string;
decimals?: number;
}) => {
const t = useT();
if (!feeFactor || !amount || !fee) return null;
if (isNaN(Number(feeFactor)) || isNaN(Number(amount)) || isNaN(Number(fee))) {
if (!feeFactor || !amount || !transferAmount || !fee) return null;
if (
isNaN(Number(feeFactor)) ||
isNaN(Number(amount)) ||
isNaN(Number(transferAmount)) ||
isNaN(Number(fee))
) {
return null;
}
const totalValue = new BigNumber(amount).plus(fee).toString();
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
<div className="mb-4 flex flex-col gap-2 text-xs">
+37 -43
View File
@@ -1,4 +1,10 @@
import { waitFor, fireEvent, render, screen } from '@testing-library/react';
import {
waitFor,
fireEvent,
render,
screen,
act,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BigNumber from 'bignumber.js';
import type { DepositFormProps } from './deposit-form';
@@ -83,10 +89,7 @@ describe('Deposit form', () => {
render(<DepositForm {...props} />);
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
// Wait for first value to show as form is rendered conditionally based on chainId
expect(
await screen.findByText('From (Ethereum address)')
).toBeInTheDocument();
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
truncateMiddle(MOCK_ETH_ADDRESS)
);
@@ -316,40 +319,34 @@ describe('Deposit form', () => {
it('shows "View asset details" button when an asset is selected', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
expect(await screen.findByTestId('view-asset-details')).toBeInTheDocument();
expect(await screen.getByTestId('view-asset-details')).toBeInTheDocument();
});
it('does not shows "View asset details" button when no asset is selected', async () => {
render(<DepositForm {...props} />);
await waitFor(() => {
expect(screen.queryAllByTestId('view-asset-details')).toHaveLength(0);
});
expect(await screen.queryAllByTestId('view-asset-details')).toHaveLength(0);
});
it('renders a connect button if Ethereum wallet is not connected', async () => {
it('renders a connect button if Ethereum wallet is not connected', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: false,
account: '',
});
render(<DepositForm {...props} />);
expect(
await screen.findByRole('button', { name: 'Connect' })
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
expect(
screen.queryByLabelText('From (Ethereum address)')
).not.toBeInTheDocument();
});
it('renders a disabled input if Ethereum wallet is connected', async () => {
it('renders a disabled input if Ethereum wallet is connected', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
});
render(<DepositForm {...props} />);
expect(await screen.findByTestId('deposit-form')).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Connect' })
).not.toBeInTheDocument();
@@ -359,56 +356,53 @@ describe('Deposit form', () => {
);
});
it('prevents submission if you are on the wrong chain', async () => {
// Make mocks return a chain id mismatch
it('prevents submission if you are on the wrong chain', () => {
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
chainId: 1,
});
(useWeb3ConnectStore as unknown as jest.Mock).mockImplementationOnce(
(useWeb3ConnectStore as unknown as jest.Mock).mockImplementation(
// eslint-disable-next-line
(selector: (result: ReturnType<typeof useWeb3ConnectStore>) => any) => {
return selector({
desiredChainId: 11155111,
open: jest.fn(),
foo: 'asdf',
});
}
);
render(<DepositForm {...props} />);
expect(await screen.findByTestId('chain-error')).toHaveTextContent(
expect(screen.getByTestId('chain-error')).toHaveTextContent(
/this app only works on/i
);
expect(screen.queryByTestId('deposit-form')).not.toBeInTheDocument();
});
it('Remaining deposit allowance tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
expect(await screen.findByTestId('deposit-form')).toBeInTheDocument();
await userEvent.hover(screen.getByText('Remaining deposit allowance'));
expect(
await screen.findByRole('tooltip', {
name: /VEGA has a lifetime deposit limit of 20 asset-symbol per address/,
})
).toBeInTheDocument();
await act(async () => {
await userEvent.hover(screen.getByText('Remaining deposit allowance'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /VEGA has a lifetime deposit limit of 20 asset-symbol per address/,
})
).toBeInTheDocument();
});
});
it('Ethereum deposit cap tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
expect(await screen.findByTestId('deposit-form')).toBeInTheDocument();
await userEvent.hover(screen.getByText('Ethereum deposit cap'));
expect(
await screen.findByRole('tooltip', {
name: /The deposit cap is set when you approve an asset for use with this app/,
})
).toBeInTheDocument();
await act(async () => {
await userEvent.hover(screen.getByText('Ethereum deposit cap'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /The deposit cap is set when you approve an asset for use with this app/,
})
).toBeInTheDocument();
});
});
});
+28 -33
View File
@@ -92,9 +92,7 @@ export const DepositForm = ({
const maxSafe = useMaxSafe();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const openDialog = useWeb3ConnectStore((store) => store.open);
const { isActive, account, chainId } = useWeb3React();
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
const invalidChain = isActive && chainId !== desiredChainId;
const { isActive, account } = useWeb3React();
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
const [approveNotificationIntent, setApproveNotificationIntent] =
useState<Intent>(Intent.Warning);
@@ -154,20 +152,7 @@ export const DepositForm = ({
const approved =
balances && balances.allowance.isGreaterThan(0) ? true : false;
return invalidChain ? (
<div className="mb-2">
<Notification
intent={Intent.Danger}
testId="chain-error"
message={t(
'This app only works on {{chainId}}. Switch your Ethereum wallet to the correct network.',
{
chainId: getChainName(desiredChainId),
}
)}
/>
</div>
) : (
return (
<form
onSubmit={handleSubmit(onSubmit)}
noValidate={true}
@@ -432,11 +417,7 @@ export const DepositForm = ({
intent={approveNotificationIntent}
amount={amount}
/>
<FormButton
approved={approved}
isActive={isActive}
selectedAsset={selectedAsset}
/>
<FormButton approved={approved} selectedAsset={selectedAsset} />
</form>
);
};
@@ -444,21 +425,35 @@ export const DepositForm = ({
interface FormButtonProps {
approved: boolean;
selectedAsset: AssetFieldsFragment | undefined;
isActive: boolean;
}
const FormButton = ({ approved, selectedAsset, isActive }: FormButtonProps) => {
const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
const t = useT();
const { isActive, chainId } = useWeb3React();
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
const invalidChain = isActive && chainId !== desiredChainId;
return (
<TradingButton
type="submit"
data-testid="deposit-submit"
fill
disabled={!isActive}
>
{t('Deposit')}
</TradingButton>
<>
{invalidChain && (
<div className="mb-2">
<Notification
intent={Intent.Danger}
testId="chain-error"
message={t('This app only works on {{chainId}}.', {
chainId: getChainName(desiredChainId),
})}
/>
</div>
)}
<TradingButton
type="submit"
data-testid="deposit-submit"
fill
disabled={!isActive || invalidChain}
>
{t('Deposit')}
</TradingButton>
</>
);
};
@@ -23,6 +23,7 @@ import {
SUBSCRIPTION_TIMEOUT,
useNodeBasicStatus,
useNodeSubscriptionStatus,
useResponseTime,
} from './row-data';
import { BLOCK_THRESHOLD, RowData } from './row-data';
import { CUSTOM_NODE_KEY } from '../../types';
@@ -161,6 +162,19 @@ describe('useNodeBasicStatus', () => {
});
});
describe('useResponseTime', () => {
it('returns response time when url is valid', () => {
const { result } = renderHook(() =>
useResponseTime('https://localhost:1234')
);
expect(result.current.responseTime).toBe(50);
});
it('does not return response time when url is invalid', () => {
const { result } = renderHook(() => useResponseTime('nope'));
expect(result.current.responseTime).toBeUndefined();
});
});
describe('RowData', () => {
const props = {
id: '0',
@@ -1,3 +1,4 @@
import { isValidUrl } from '@vegaprotocol/utils';
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react';
import { CUSTOM_NODE_KEY } from '../../types';
@@ -7,7 +8,6 @@ import {
} from '../../utils/__generated__/NodeCheck';
import { LayoutCell } from './layout-cell';
import { useT } from '../../use-t';
import { useResponseTime } from '../../utils/time';
export const POLL_INTERVAL = 1000;
export const SUBSCRIPTION_TIMEOUT = 3000;
@@ -108,6 +108,20 @@ export const useNodeBasicStatus = () => {
};
};
export const useResponseTime = (url: string, trigger?: unknown) => {
const [responseTime, setResponseTime] = useState<number>();
useEffect(() => {
if (!isValidUrl(url)) return;
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
const { duration } =
(requests.length && requests[requests.length - 1]) || {};
setResponseTime(duration);
}, [url, trigger]);
return { responseTime };
};
export const RowData = ({
id,
url,
@@ -10,7 +10,6 @@ import {
getUserEnabledFeatureFlags,
setUserEnabledFeatureFlag,
} from './use-environment';
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
const noop = () => {
/* no op*/
@@ -18,10 +17,6 @@ const noop = () => {
jest.mock('@vegaprotocol/apollo-client');
jest.mock('zustand');
jest.mock('../utils/time');
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
const mockCreateClient = createClient as jest.Mock;
const createDefaultMockClient = () => {
@@ -160,14 +155,6 @@ describe('useEnvironment', () => {
const fastNode = 'https://api.n01.foo.vega.xyz';
const fastWait = 1000;
const nodes = [slowNode, fastNode];
mockCanMeasureResponseTime.mockImplementation(() => true);
mockMeasureResponseTime.mockImplementation((url: string) => {
if (url === slowNode) return slowWait;
if (url === fastNode) return fastWait;
return Infinity;
});
// @ts-ignore: typscript doesn't recognise the mock implementation
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
@@ -181,7 +168,7 @@ describe('useEnvironment', () => {
statistics: {
chainId: 'chain-id',
blockHeight: '100',
vegaTime: new Date(1).toISOString(),
vegaTime: new Date().toISOString(),
},
},
});
@@ -209,8 +196,7 @@ describe('useEnvironment', () => {
expect(result.current.nodes).toEqual(nodes);
});
jest.advanceTimersByTime(2000);
// jest.runAllTimers();
jest.runAllTimers();
await waitFor(() => {
expect(result.current.status).toEqual('success');
+29 -78
View File
@@ -19,9 +19,6 @@ import { compileErrors } from '../utils/compile-errors';
import { envSchema } from '../utils/validate-environment';
import { tomlConfigSchema } from '../utils/validate-configuration';
import uniq from 'lodash/uniq';
import orderBy from 'lodash/orderBy';
import first from 'lodash/first';
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
type Client = ReturnType<typeof createClient>;
type ClientCollection = {
@@ -41,17 +38,8 @@ export type EnvStore = Env & Actions;
const VERSION = 1;
export const STORAGE_KEY = `vega_url_${VERSION}`;
const QUERY_TIMEOUT = 3000;
const SUBSCRIPTION_TIMEOUT = 3000;
const raceAgainst = (timeout: number): Promise<false> =>
new Promise((resolve) => {
setTimeout(() => {
resolve(false);
}, timeout);
});
/**
* Fetch and validate a vega node configuration
*/
@@ -76,88 +64,53 @@ const fetchConfig = async (url?: string) => {
const findNode = async (clients: ClientCollection): Promise<string | null> => {
const tests = Object.entries(clients).map((args) => testNode(...args));
try {
const nodes = await Promise.all(tests);
const responsiveNodes = nodes
.filter(([, q, s]) => q && s)
.map(([url, q]) => {
return {
url,
...q,
};
});
// more recent and faster at the top
const ordered = orderBy(
responsiveNodes,
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
['desc', 'desc', 'asc']
);
const best = first(ordered);
return best ? best.url : null;
} catch (err) {
const url = await Promise.any(tests);
return url;
} catch {
// All tests rejected, no suitable node found
return null;
}
};
type Maybe<T> = T | false;
type QueryTestResult = {
blockHeight: number;
vegaTime: Date;
responseTime: number;
};
type SubscriptionTestResult = true;
type NodeTestResult = [
/** url */
string,
Maybe<QueryTestResult>,
Maybe<SubscriptionTestResult>
];
/**
* Test a node for suitability for connection
*/
const testNode = async (
url: string,
client: Client
): Promise<NodeTestResult> => {
): Promise<string | null> => {
const results = await Promise.all([
testQuery(client, url),
// these promises will only resolve with true/false
testQuery(client),
testSubscription(client),
]);
return [url, ...results];
if (results[0] && results[1]) {
return url;
}
const message = `Tests failed for node: ${url}`;
console.warn(message);
// throwing here will mean this tests is ignored and a different
// node that hopefully does resolve will fulfill the Promise.any
throw new Error(message);
};
/**
* Run a test query on a client
*/
const testQuery = (
client: Client,
url: string
): Promise<Maybe<QueryTestResult>> => {
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
client
.query<NodeCheckQuery>({
query: NodeCheckDocument,
})
.then((result) => {
if (result && !result.error) {
const res = {
blockHeight: Number(result.data.statistics.blockHeight),
vegaTime: new Date(result.data.statistics.vegaTime),
// only after a request has been sent we can retrieve the response time
responseTime: canMeasureResponseTime(url)
? measureResponseTime(url) || Infinity
: Infinity,
} as QueryTestResult;
resolve(res);
} else {
resolve(false);
}
})
.catch(() => resolve(false))
);
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
const testQuery = async (client: Client) => {
try {
const result = await client.query<NodeCheckQuery>({
query: NodeCheckDocument,
});
if (!result || result.error) {
return false;
}
return true;
} catch (err) {
return false;
}
};
/**
@@ -165,9 +118,7 @@ const testQuery = (
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
* is deemed a failure
*/
const testSubscription = (
client: Client
): Promise<Maybe<SubscriptionTestResult>> => {
const testSubscription = (client: Client) => {
return new Promise((resolve) => {
const sub = client
.subscribe<NodeCheckTimeUpdateSubscription>({
-1
View File
@@ -131,7 +131,6 @@ export const useEtherscanLink = () => {
export const CONSOLE_TRANSFER = '#/portfolio/assets/transfer';
export const CONSOLE_TRANSFER_ASSET =
'#/portfolio/assets/transfer?assetId=:assetId';
export const CONSOLE_MARKET_PAGE = '#/markets/:marketId';
// Governance pages
export const TOKEN_NEW_MARKET_PROPOSAL = '/proposals/propose/new-market';
-22
View File
@@ -1,22 +0,0 @@
import { renderHook } from '@testing-library/react';
import { useResponseTime } from './time';
const mockResponseTime = 50;
global.performance.getEntriesByName = jest.fn().mockReturnValue([
{
duration: mockResponseTime,
},
]);
describe('useResponseTime', () => {
it('returns response time when url is valid', () => {
const { result } = renderHook(() =>
useResponseTime('https://localhost:1234')
);
expect(result.current.responseTime).toBe(50);
});
it('does not return response time when url is invalid', () => {
const { result } = renderHook(() => useResponseTime('nope'));
expect(result.current.responseTime).toBeUndefined();
});
});
-25
View File
@@ -1,25 +0,0 @@
import { isValidUrl } from '@vegaprotocol/utils';
import { useEffect, useState } from 'react';
export const useResponseTime = (url: string, trigger?: unknown) => {
const [responseTime, setResponseTime] = useState<number>();
useEffect(() => {
if (!canMeasureResponseTime(url)) return;
const duration = measureResponseTime(url);
setResponseTime(duration);
}, [url, trigger]);
return { responseTime };
};
export const canMeasureResponseTime = (url: string) => {
if (!isValidUrl(url)) return false;
if (typeof window.performance.getEntriesByName !== 'function') return false;
return true;
};
export const measureResponseTime = (url: string) => {
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
const { duration } = (requests.length && requests[requests.length - 1]) || {};
return duration;
};
+2
View File
@@ -21,6 +21,7 @@
"Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.": "Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.",
"Enter manually": "Enter manually",
"From account": "From account",
"Include transfer fee": "Include transfer fee",
"initial level": "initial level",
"maintenance level": "maintenance level",
"Margin health": "Margin health",
@@ -32,6 +33,7 @@
"release level": "release level",
"search level": "search level",
"Select from wallet": "Select from wallet",
"The fee will be taken from the amount you are transferring.": "The fee will be taken from the amount you are transferring.",
"The total amount of each asset on this key. Includes used and available collateral.": "The total amount of each asset on this key. Includes used and available collateral.",
"The total amount taken from your account. The amount to be transferred plus the fee.": "The total amount taken from your account. The amount to be transferred plus the fee.",
"The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)",
+1 -1
View File
@@ -27,7 +27,7 @@
"The {{symbol}} faucet is not available at this time": "The {{symbol}} faucet is not available at this time",
"The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve {{assetSymbol}} again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.": "The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve {{assetSymbol}} again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.",
"The faucet transaction was rejected by the connected Ethereum wallet": "The faucet transaction was rejected by the connected Ethereum wallet",
"This app only works on {{chainId}}. Switch your Ethereum wallet to the correct network.": "This app only works on {{chainId}}. Switch your Ethereum wallet to the correct network.",
"This app only works on {{chainId}}.": "This app only works on {{chainId}}.",
"To (Vega key)": "To (Vega key)",
"To date, {{currentDeposit}} {{assetSymbol}} has been deposited from this Ethereum address, so you can deposit up to {{remainingDeposit}} {{assetSymbol}} more.": "To date, {{currentDeposit}} {{assetSymbol}} has been deposited from this Ethereum address, so you can deposit up to {{remainingDeposit}} {{assetSymbol}} more.",
"Use maximum": "Use maximum",
-2
View File
@@ -799,8 +799,6 @@
"unsupportedVersion": "Looks like you're running an outdated version of GoWallet. You're running {{version}} but {{requiredVersion}} is required.",
"UpdateAsset": "Update asset",
"UpdateAssetProposal": "Update asset proposal",
"UpdateToMarket": "Update to market ID",
"OpenInConsole": "Open in Console",
"UpdateMarket": "Update market",
"UpdateMarketProposal": "Update market proposal",
"UpdateMarketState": "Update market state",
-1
View File
@@ -30,7 +30,6 @@
"How often the quality of liquidity supplied by each liquidity provider is evaluated and the fees arising from that period are earmarked for specific providers. This is a market parameter. ": "How often the quality of liquidity supplied by each liquidity provider is evaluated and the fees arising from that period are earmarked for specific providers. This is a market parameter. ",
"Instrument": "Instrument",
"Insurance pool": "Insurance pool",
"Insurance Pool Balance": "Insurance Pool Balance",
"Internal conditions": "Internal conditions",
"Invalid data source": "Invalid data source",
"involvedInMarkets_one": "Involved in {{count}} market",
+1 -1
View File
@@ -300,7 +300,7 @@
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"Vega Reward pot": "Vega Reward pot",
"Vega Wallet <0>full featured</0>": "Vega Wallet <0>full featured</0>",
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
"Vega chart": "Vega chart",
"Vesting": "Vesting",
"Vesting multiplier": "Vesting multiplier",
+1 -1
View File
@@ -53,7 +53,7 @@
"Use the Desktop App/CLI": "Use the Desktop App/CLI",
"User rejected": "User rejected",
"Vega browser extension not installed": "Vega browser extension not installed",
"Vega Wallet <0>full featured</0>": "Vega Wallet <0>full featured</0>",
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
"Verifying chain": "Verifying chain",
"View as party": "View as party",
"VIEW AS VEGA USER": "VIEW AS VEGA USER",
@@ -139,6 +139,7 @@ query MarketInfo($marketId: ID!) {
state
tradingMode
linearSlippageFactor
quadraticSlippageFactor
proposal {
id
rationale {
File diff suppressed because one or more lines are too long
@@ -177,7 +177,7 @@ export const InsurancePoolInfoPanel = ({
return (
<MarketInfoTable
data={{
insurancePoolBalance: account.balance,
balance: account.balance,
}}
assetSymbol={asset.symbol}
decimalPlaces={asset.decimals}
@@ -551,6 +551,7 @@ export const MarginScalingFactorsPanel = ({
}: MarketInfoProps) => {
const data = {
linearSlippageFactor: market.linearSlippageFactor,
quadraticSlippageFactor: market.quadraticSlippageFactor,
searchLevel:
market.tradableInstrument.marginCalculator?.scalingFactors.searchLevel,
initialMargin:
@@ -563,6 +564,7 @@ export const MarginScalingFactorsPanel = ({
const parentData = parentMarket
? {
linearSlippageFactor: parentMarket?.linearSlippageFactor,
quadraticSlippageFactor: parentMarket?.quadraticSlippageFactor,
searchLevel:
parentMarket?.tradableInstrument.marginCalculator?.scalingFactors
.searchLevel,

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