Compare commits

..
89 changed files with 995 additions and 21076 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
@@ -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);
@@ -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
@@ -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,117 +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
);
const assetInSettledMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state &&
[
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_SETTLED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(m.state)
);
}
return false;
});
if (marketSettled) {
return null;
}
// Gray out the cards that are related to suspended markets
const suspended = transferNode.markets?.some(
(m) =>
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
const assetInSuspendedMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
}
return false;
});
// Gray out the cards that are related to suspended markets
const { gradientClassName, mainClassName } =
suspended || assetInSuspendedMarket || assetInSettledMarket
? {
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>
@@ -435,20 +373,8 @@ export const ActiveRewardCard = ({
<span className="border-[0.5px] border-gray-700" />
<span>
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<Tooltip
underline={suspended}
description={
(suspended || assetInSuspendedMarket) &&
(specificMarkets
? t('Eligible market(s) currently suspended')
: assetInSuspendedMarket
? 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">
@@ -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}")
@@ -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"
@@ -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")
@@ -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} />;
-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';
-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
@@ -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,
@@ -24,6 +24,7 @@ export const marketInfoQuery = (
},
},
linearSlippageFactor: '0.01',
quadraticSlippageFactor: '0.0001',
marketTimestamps: {
__typename: 'MarketTimestamps',
open: '2022-11-15T02:15:24.543614154Z',
@@ -137,6 +137,7 @@ fragment NewMarketFields on NewMarket {
# auctionExtensionSecs
# }
# linearSlippageFactor
# quadraticSlippageFactor
successorConfiguration {
parentMarketId
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__", "charting-library.d.ts"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
File diff suppressed because it is too large Load Diff
@@ -9,17 +9,17 @@ export const TradingViewContainer = ({
libraryHash,
marketId,
interval,
studies,
onIntervalChange,
onAutoSaveNeeded,
state,
}: {
libraryPath: string;
libraryHash: string;
marketId: string;
interval: ResolutionString;
studies: string[];
onIntervalChange: (interval: string) => void;
onAutoSaveNeeded: OnAutoSaveNeededCallback;
state: object | undefined;
}) => {
const t = useT();
const scriptState = useScript(
@@ -48,9 +48,9 @@ export const TradingViewContainer = ({
libraryPath={libraryPath}
marketId={marketId}
interval={interval}
studies={studies}
onIntervalChange={onIntervalChange}
onAutoSaveNeeded={onAutoSaveNeeded}
state={state}
/>
);
};
+82 -122
View File
@@ -1,167 +1,127 @@
import { useEffect, useRef } from 'react';
import {
usePrevious,
useScreenDimensions,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import { useLanguage } from './use-t';
import { useDatafeed } from './use-datafeed';
import { type ResolutionString } from './constants';
import {
type ChartingLibraryFeatureset,
type LanguageCode,
type ChartingLibraryWidgetOptions,
type IChartingLibraryWidget,
type ChartPropertiesOverrides,
type ResolutionString as TVResolutionString,
} from '../charting-library';
const noop = () => {};
export type OnAutoSaveNeededCallback = (data: object) => void;
export type OnAutoSaveNeededCallback = (data: { studies: string[] }) => void;
export const TradingView = ({
marketId,
libraryPath,
interval,
studies,
onIntervalChange,
onAutoSaveNeeded,
state,
}: {
marketId: string;
libraryPath: string;
interval: ResolutionString;
studies: string[];
onIntervalChange: (interval: string) => void;
onAutoSaveNeeded: OnAutoSaveNeededCallback;
state: object | undefined;
}) => {
const { isMobile } = useScreenDimensions();
const { theme } = useThemeSwitcher();
const language = useLanguage();
const chartContainerRef = useRef<HTMLDivElement>(null);
const widgetRef = useRef<IChartingLibraryWidget>();
const chartContainerRef =
useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>;
// Cant get types as charting_library is externally loaded
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const widgetRef = useRef<any>();
const datafeed = useDatafeed();
const prevMarketId = usePrevious(marketId);
const prevTheme = usePrevious(theme);
useEffect(
() => {
const disableOnSmallScreens = isMobile ? ['left_toolbar'] : [];
useEffect(() => {
// Widget already created
if (widgetRef.current !== undefined) {
// Update the symbol if changed
if (marketId !== prevMarketId) {
widgetRef.current.setSymbol(
marketId,
(interval ? interval : '15') as TVResolutionString,
noop
);
}
const overrides = getOverrides(theme);
// Update theme theme if changed
if (theme !== prevTheme) {
widgetRef.current.changeTheme(theme).then(() => {
if (!widgetRef.current) return;
widgetRef.current.applyOverrides(getOverrides(theme));
const widgetOptions = {
symbol: marketId,
datafeed,
interval: interval,
container: chartContainerRef.current,
library_path: libraryPath,
custom_css_url: 'vega_styles.css',
// Trading view accepts just 'en' rather than 'en-US' which is what react-i18next provides
// https://www.tradingview.com/charting-library-docs/latest/core_concepts/Localization?_highlight=language#supported-languages
locale: language.split('-')[0],
enabled_features: ['tick_resolution'],
disabled_features: [
'header_symbol_search',
'header_compare',
'show_object_tree',
'timeframes_toolbar',
...disableOnSmallScreens,
],
fullscreen: false,
autosize: true,
theme,
overrides,
loading_screen: {
backgroundColor: overrides['paneProperties.background'],
},
};
// @ts-ignore parent component loads TradingView onto window obj
widgetRef.current = new window.TradingView.widget(widgetOptions);
widgetRef.current.onChartReady(() => {
widgetRef.current.applyOverrides(getOverrides(theme));
widgetRef.current.subscribe('onAutoSaveNeeded', () => {
const studies = widgetRef.current
.activeChart()
.getAllStudies()
.map((s: { id: string; name: string }) => s.name);
onAutoSaveNeeded({ studies });
});
}
return;
}
const activeChart = widgetRef.current.activeChart();
if (!chartContainerRef.current) {
return;
}
// Show volume study by default, second bool arg adds it as a overlay on top of the chart
studies.forEach((study) => {
activeChart.createStudy(study);
});
// Create widget
const overrides = getOverrides(theme);
const disabledOnSmallScreens: ChartingLibraryFeatureset[] = isMobile
? ['left_toolbar']
: [];
const disabledFeatures: ChartingLibraryFeatureset[] = [
'header_symbol_search',
'header_compare',
'show_object_tree',
'timeframes_toolbar',
...disabledOnSmallScreens,
];
const widgetOptions: ChartingLibraryWidgetOptions = {
symbol: marketId,
datafeed,
interval: interval as TVResolutionString,
container: chartContainerRef.current,
library_path: libraryPath,
custom_css_url: 'vega_styles.css',
// Trading view accepts just 'en' rather than 'en-US' which is what react-i18next provides
// https://www.tradingview.com/charting-library-docs/latest/core_concepts/Localization?_highlight=language#supported-languages
locale: language.split('-')[0] as LanguageCode,
enabled_features: ['tick_resolution'],
disabled_features: disabledFeatures,
fullscreen: false,
autosize: true,
theme,
overrides,
loading_screen: {
backgroundColor: overrides['paneProperties.background'],
},
auto_save_delay: 1,
saved_data: state,
};
widgetRef.current = new window.TradingView.widget(widgetOptions);
widgetRef.current.onChartReady(() => {
if (!widgetRef.current) return;
const activeChart = widgetRef.current.activeChart();
if (!state) {
// If chart has loaded with no state, create a volume study
activeChart.createStudy('Volume');
}
// Subscribe to interval changes so it can be persisted in chart settings
activeChart.onIntervalChanged().subscribe(null, onIntervalChange);
});
widgetRef.current.subscribe('onAutoSaveNeeded', () => {
if (!widgetRef.current) return;
widgetRef.current.save((newState) => {
onAutoSaveNeeded(newState);
// Subscribe to interval changes so it can be persisted in chart settings
activeChart.onIntervalChanged().subscribe(null, onIntervalChange);
});
});
}, [
state,
datafeed,
interval,
prevTheme,
prevMarketId,
marketId,
theme,
language,
libraryPath,
isMobile,
onAutoSaveNeeded,
onIntervalChange,
]);
return () => {
if (!widgetRef.current) return;
widgetRef.current.remove();
};
},
// No theme in deps to avoid full chart reload when the theme changes
// Instead the theme is changed programmitcally in a separate useEffect
// eslint-disable-next-line react-hooks/exhaustive-deps
[datafeed, marketId, language, libraryPath, isMobile]
);
// Update the trading view theme every time the app theme updates, doen separately
// to avoid full chart reload
useEffect(() => {
return () => {
if (!widgetRef.current) return;
widgetRef.current.remove();
widgetRef.current = undefined;
};
}, []);
if (!widgetRef.current || !widgetRef.current._ready) return;
// Calling changeTheme will reset the default dark/light background to the TV default
// so we need to re-apply the pane bg override. A promise is also required
// https://github.com/tradingview/charting_library/issues/6546#issuecomment-1139517908
widgetRef.current.changeTheme(theme).then(() => {
widgetRef.current.applyOverrides(getOverrides(theme));
});
}, [theme]);
return <div ref={chartContainerRef} className="w-full h-full" />;
};
const getOverrides = (
theme: 'dark' | 'light'
): Partial<ChartPropertiesOverrides> => {
const getOverrides = (theme: 'dark' | 'light') => {
return {
// colors set here, trading view lets the user set a color
'paneProperties.background': theme === 'dark' ? '#05060C' : '#fff',
+33 -10
View File
@@ -2,6 +2,15 @@ import { useEffect, useMemo, useRef } from 'react';
import compact from 'lodash/compact';
import { useApolloClient } from '@apollo/client';
import { type Subscription } from 'zen-observable-ts';
/*
* TODO: figure out how we can get the chart types
import {
type LibrarySymbolInfo,
type IBasicDataFeed,
type ResolutionString,
type SeriesFormat,
} from '../charting_library/charting_library';
*/
import {
GetBarsDocument,
LastBarDocument,
@@ -18,12 +27,6 @@ import {
type SymbolQueryVariables,
} from './__generated__/Symbol';
import { getMarketExpiryDate, toBigNum } from '@vegaprotocol/utils';
import {
type IBasicDataFeed,
type DatafeedConfiguration,
type LibrarySymbolInfo,
type ResolutionString,
} from '../charting-library';
const EXCHANGE = 'VEGA';
@@ -39,8 +42,12 @@ const resolutionMap: Record<string, Interval> = {
const supportedResolutions = Object.keys(resolutionMap);
const configurationData: DatafeedConfiguration = {
const configurationData = {
// only showing Vega ofc
exchanges: [EXCHANGE],
// Represents the resolutions for bars supported by your datafeed
// @ts-ignore cant import types as chartin_library is external
supported_resolutions: supportedResolutions as ResolutionString[],
} as const;
@@ -50,7 +57,9 @@ export const useDatafeed = () => {
const client = useApolloClient();
const datafeed = useMemo(() => {
// @ts-ignore cant import types as chartin_library is external
const feed: IBasicDataFeed = {
// @ts-ignore cant import types as chartin_library is external
onReady: (callback) => {
setTimeout(() => callback(configurationData));
},
@@ -60,8 +69,11 @@ export const useDatafeed = () => {
},
resolveSymbol: async (
// @ts-ignore cant import types as chartin_library is external
marketId,
// @ts-ignore cant import types as chartin_library is external
onSymbolResolvedCallback,
// @ts-ignore cant import types as chartin_library is external
onResolveErrorCallback
) => {
try {
@@ -98,8 +110,9 @@ export const useDatafeed = () => {
const expirationDate = getMarketExpiryDate(instrument.metadata.tags);
const expirationTimestamp = expirationDate
? Math.floor(expirationDate.getTime() / 1000)
: undefined;
: null;
// @ts-ignore cant import types as chartin_library is external
const symbolInfo: LibrarySymbolInfo = {
ticker: market.id, // use ticker as our unique identifier so that code/name can be used for name/description
name: instrument.code,
@@ -107,9 +120,10 @@ export const useDatafeed = () => {
description: instrument.name,
listed_exchange: EXCHANGE,
expired: productType === 'Perpetual' ? false : true,
expiration_date: expirationTimestamp,
expirationDate: expirationTimestamp,
format: 'price',
// @ts-ignore cant import types as chartin_library is external
format: 'price' as SeriesFormat,
type,
session: '24x7',
timezone: 'Etc/UTC',
@@ -137,10 +151,15 @@ export const useDatafeed = () => {
},
getBars: async (
// @ts-ignore cant import types as chartin_library is external
symbolInfo,
// @ts-ignore cant import types as chartin_library is external
resolution,
// @ts-ignore cant import types as chartin_library is external
periodParams,
// @ts-ignore cant import types as chartin_library is external
onHistoryCallback,
// @ts-ignore cant import types as chartin_library is external
onErrorCallback
) => {
if (!symbolInfo.ticker) {
@@ -192,9 +211,13 @@ export const useDatafeed = () => {
},
subscribeBars: (
// @ts-ignore cant import types as chartin_library is external
symbolInfo,
// @ts-ignore cant import types as chartin_library is external
resolution,
// @ts-ignore cant import types as chartin_library is external
onTick
// subscriberUID, // chart will subscribe and unsbuscribe when the parent market of the page changes so we don't need to use subscriberUID as of now
) => {
if (!symbolInfo.ticker) {
@@ -16,7 +16,6 @@ type TradingButtonProps = {
subLabel?: ReactNode;
fill?: boolean;
minimal?: boolean;
testId?: string;
};
const getClassName = (
@@ -121,7 +120,6 @@ export const TradingButton = forwardRef<
className,
subLabel,
fill,
testId,
...props
},
ref
@@ -134,7 +132,6 @@ export const TradingButton = forwardRef<
{ size, subLabel, intent, fill, minimal },
className
)}
data-testid={testId}
{...props}
>
<Content icon={icon} subLabel={subLabel} children={children} />
-8
View File
@@ -84,14 +84,6 @@ describe('number utils', () => {
expect(formatNumberPercentage(v, d)).toStrictEqual(o);
});
it('formatNumberPercentage returns "-" when value is null', () => {
expect(formatNumberPercentage(null)).toStrictEqual('-');
});
it('formatNumberPercentage returns "-" when value is undefined', () => {
expect(formatNumberPercentage(undefined)).toStrictEqual('-');
});
describe('toNumberParts', () => {
it.each([
{ v: null, d: 3, o: ['0', '000', '.'] },
+1 -8
View File
@@ -156,14 +156,7 @@ export const addDecimalsFixedFormatNumber = (
return formatNumberFixed(x, formatDecimals);
};
export const formatNumberPercentage = (
value: BigNumber | null | undefined,
decimals?: number
) => {
if (!value) {
return '-';
}
export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
const decimalPlaces =
typeof decimals === 'undefined' ? value.dp() || 0 : decimals;
return `${formatNumber(value, decimalPlaces)}%`;
@@ -22,7 +22,7 @@ const mockUpdateDialogOpen = jest.fn();
const mockCloseVegaDialog = jest.fn();
let mockIsDesktopRunning = true;
const mockChainId = 'VEGA_CHAIN_ID';
const mockChainId = 'chain-id';
jest.mock('../use-is-wallet-service-running', () => ({
useIsWalletServiceRunning: jest
@@ -30,6 +30,10 @@ jest.mock('../use-is-wallet-service-running', () => ({
.mockImplementation(() => mockIsDesktopRunning),
}));
jest.mock('./use-chain-id', () => ({
useChainId: jest.fn().mockImplementation(() => mockChainId),
}));
let defaultProps: VegaConnectDialogProps;
const INITIAL_KEY = 'some-key';
@@ -67,7 +71,6 @@ const defaultConfig: VegaWalletConfig = {
chromeExtensionUrl: 'chrome-link',
mozillaExtensionUrl: 'mozilla-link',
},
chainId: 'VEGA_CHAIN_ID',
};
function generateJSX(
@@ -202,21 +205,11 @@ describe('VegaConnectDialog', () => {
});
it('handles user rejection', async () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-015
// 0003-WTXN-007
spyOnConnectWallet
.mockClear()
.mockImplementation(() =>
delayedReject(
new WalletError(
'User error',
3001,
'The user rejected the request'
),
delay
new WalletError('User error', 3001, 'The user rejected the request')
)
);
@@ -245,10 +238,6 @@ describe('VegaConnectDialog', () => {
});
it('handles an unknown error', async () => {
// 0003-WTXN-009
// 0003-WTXN-011
// 0002-WCON-016
// 0003-WTXN-008
spyOnCheckCompat.mockClear().mockImplementation(() => {
throw new Error('unknown error');
});
@@ -325,6 +314,13 @@ describe('VegaConnectDialog', () => {
render(generateJSX());
await selectInjected();
// Chain check
expect(screen.getByText('Verifying chain')).toBeInTheDocument();
expect(vegaWindow.getChainId).toHaveBeenCalled();
await act(async () => {
jest.advanceTimersByTime(delay);
});
// Await user connect
expect(screen.getByText('Connecting...')).toBeInTheDocument();
expect(vegaWindow.connectWallet).toHaveBeenCalled();
@@ -345,6 +341,43 @@ describe('VegaConnectDialog', () => {
expect(mockCloseVegaDialog).toHaveBeenCalledWith();
});
it('handles invalid chain', async () => {
const delay = 100;
const invalidChain = 'invalid chain';
const vegaWindow = {
getChainId: jest.fn(() =>
delayedResolve({ chainID: invalidChain }, delay)
),
connectWallet: jest.fn(() => delayedResolve(null, delay)),
disconnectWallet: jest.fn(() => delayedResolve(undefined, delay)),
listKeys: jest.fn(() =>
delayedResolve(
{
keys: [{ name: 'test key', publicKey: '0x123' }],
},
100
)
),
};
mockBrowserWallet(vegaWindow);
render(generateJSX());
await selectInjected();
// Chain check
expect(screen.getByText('Verifying chain')).toBeInTheDocument();
expect(vegaWindow.getChainId).toHaveBeenCalled();
await act(async () => {
jest.advanceTimersByTime(delay);
});
expect(screen.getByText('Wrong network')).toBeInTheDocument();
expect(
screen.getByText(
new RegExp(`set your wallet network in your app to "${mockChainId}"`)
)
).toBeInTheDocument();
});
async function selectInjected() {
expect(await screen.findByRole('dialog')).toBeInTheDocument();
fireEvent.click(await screen.findByTestId('connector-injected'));
@@ -44,6 +44,7 @@ import { isBrowserWalletInstalled } from '../utils';
import { useIsWalletServiceRunning } from '../use-is-wallet-service-running';
import { SnapStatus, useSnapStatus } from '../use-snap-status';
import { useVegaWalletDialogStore } from './vega-wallet-dialog-store';
import { useChainId } from './use-chain-id';
import { useT } from '../use-t';
import { Trans } from 'react-i18next';
@@ -64,7 +65,7 @@ export const VegaConnectDialog = ({
contentOnly,
onClose,
}: VegaConnectDialogProps) => {
const { chainId, disconnect, acknowledgeNeeded } = useVegaWallet();
const { disconnect, acknowledgeNeeded } = useVegaWallet();
const vegaWalletDialogOpen = useVegaWalletDialogStore(
(store) => store.vegaWalletDialogOpen
);
@@ -84,6 +85,10 @@ export const VegaConnectDialog = ({
[updateVegaWalletDialog, acknowledgeNeeded, disconnect]
);
// Ensure we have a chain Id so we can compare with wallet chain id.
// This value will already be in the cache, if it failed the app wont render
const chainId = useChainId();
const content = chainId && (
<ConnectDialogContainer
connectors={connectors}
@@ -263,7 +268,7 @@ const ConnectorList = ({
onClick={() => onSelect('injected')}
title={
<Trans
defaults="Vega Wallet <0>full featured</0>"
defaults="Vega Wallet <0>full featured<0>"
components={[<span className="text-xs">full featured</span>]}
/>
}
@@ -276,7 +281,7 @@ const ConnectorList = ({
<div>
<h1 className="mb-1 text-lg">
<Trans
defaults="Vega Wallet <0>full featured</0>"
defaults="Vega Wallet <0>full featured<0>"
components={[<span className="text-xs">full featured</span>]}
/>
</h1>
@@ -0,0 +1,72 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useVegaWallet } from '../use-vega-wallet';
import { useChainId } from './use-chain-id';
global.fetch = jest.fn();
const mockFetch = global.fetch as jest.Mock;
mockFetch.mockImplementation((url: string) => {
return Promise.resolve({ ok: true });
});
jest.mock('../use-vega-wallet', () => {
const original = jest.requireActual('../use-vega-wallet');
return {
...original,
useVegaWallet: jest.fn(),
};
});
describe('useChainId', () => {
it('does not call fetch when statistics url could not be determined', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: '',
});
renderHook(() => useChainId());
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledTimes(0);
});
});
it('calls fetch with correct statistics url', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: 'http://localhost:1234/graphql',
});
renderHook(() => useChainId());
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:1234/statistics'
);
});
});
it('does not return chain id when chain id is not present in response', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: 'http://localhost:1234/graphql',
});
const { result } = renderHook(() => useChainId());
await waitFor(() => {
expect(result.current).toBeUndefined();
});
});
it('returns chain id when chain id is present in response', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: 'http://localhost:1234/graphql',
});
mockFetch.mockImplementation(() => {
return Promise.resolve({
ok: true,
json: () => ({
statistics: {
chainId: '1234',
},
}),
});
});
const { result } = renderHook(() => useChainId());
await waitFor(() => {
expect(result.current).not.toBeUndefined();
expect(result.current).toEqual('1234');
});
});
});
@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
export const MAX_FETCH_ATTEMPTS = 3;
import { useVegaWallet } from '../use-vega-wallet';
const cache: Record<string, string> = {};
@@ -19,17 +18,16 @@ const getNodeStatisticsUrl = (vegaUrl: string) => {
}
};
export const useChainId = (vegaUrl: string | undefined) => {
const [chainId, setChainId] = useState<undefined | string>(
vegaUrl ? cache[vegaUrl] : undefined
);
const [fetchAttempts, setFetchAttempts] = useState(1);
export const useChainId = () => {
const { vegaUrl } = useVegaWallet();
const [chainId, setChainId] = useState<undefined | string>(cache[vegaUrl]);
const [fetchAttempt, setFetchAttempt] = useState(1);
const statisticsUrl = vegaUrl ? getNodeStatisticsUrl(vegaUrl) : undefined;
const statisticsUrl = getNodeStatisticsUrl(vegaUrl);
useEffect(() => {
// abort when `/statistics` URL could not be determined
if (!statisticsUrl || !vegaUrl) return;
if (!statisticsUrl) return;
let isCancelled = false;
if (cache[vegaUrl]) {
setChainId(cache[vegaUrl]);
@@ -44,19 +42,16 @@ export const useChainId = (vegaUrl: string | undefined) => {
if (!response?.statistics?.chainId) {
throw new Error('statistics.chainId not present in fetched response');
}
const chainId = response.statistics.chainId;
cache[vegaUrl] = chainId;
setChainId(chainId);
setChainId(response?.statistics?.chainId);
})
.catch(() => {
if (fetchAttempts < MAX_FETCH_ATTEMPTS) {
setFetchAttempts((value) => (value += 1));
if (fetchAttempt < 3) {
setFetchAttempt((value) => (value += 1));
}
});
return () => {
isCancelled = true;
};
}, [fetchAttempts, statisticsUrl, vegaUrl]);
}, [fetchAttempt, statisticsUrl, vegaUrl]);
return chainId;
};
@@ -1,11 +1,10 @@
import { clearConfig, setConfig } from '../storage';
import type { Transaction, VegaConnector } from './vega-connector';
type VegaWalletEvent = 'client.disconnected';
declare global {
interface Vega {
connectWallet: (args: { chainId: string }) => Promise<null>;
getChainId: () => Promise<{ chainID: string }>;
connectWallet: () => Promise<null>;
disconnectWallet: () => Promise<void>;
listKeys: () => Promise<{
keys: Array<{ name: string; publicKey: string }>;
@@ -35,9 +34,6 @@ declare global {
};
transactionHash: string;
}>;
on: (event: VegaWalletEvent, callback: () => void) => void;
isConnected?: () => Promise<boolean>;
}
interface Window {
@@ -51,60 +47,15 @@ export const InjectedConnectorErrors = {
INVALID_CHAIN: new Error('Invalid chain'),
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const wait = (ms: number) =>
new Promise<boolean>((_, reject) => {
setTimeout(() => {
reject(false);
}, ms);
});
const INJECTED_CONNECTOR_TIMEOUT = 1000;
export class InjectedConnector implements VegaConnector {
isConnected = false;
chainId: string | null = null;
description = 'Connects using the Vega wallet browser extension';
alive: ReturnType<typeof setInterval> | undefined = undefined;
async connectWallet(chainId: string) {
this.chainId = chainId;
try {
await window.vega.connectWallet({ chainId });
this.isConnected = true;
window.vega.on('client.disconnected', () => {
this.isConnected = false;
});
async getChainId() {
return window.vega.getChainId();
}
this.alive = setInterval(async () => {
try {
const connected = await Promise.race([
// FIXME: All of the `window.vega` initiated promises are `pending`
// while waiting for the user action when transaction is sent.
// (Probably due to the FIFO queue of the `PortServer`?)
// Because of that we cannot `wait` here as while waiting for the
// user action in wallet this will `reject`. It'd be cool if the
// `window.vega` was not blocking the api calls.
// wait(INJECTED_CONNECTOR_TIMEOUT),
// `isConnected` is only available in the newer versions
// of the browser wallet
'isConnected' in window.vega &&
typeof window.vega.isConnected === 'function'
? window.vega.isConnected()
: window.vega.listKeys(),
]);
this.isConnected = Boolean(connected);
} catch {
this.isConnected = false;
}
}, INJECTED_CONNECTOR_TIMEOUT * 2);
} catch {
throw new Error(
`could not connect to the vega wallet on chain: ${chainId}`
);
}
connectWallet() {
return window.vega.connectWallet();
}
async connect() {
@@ -118,11 +69,19 @@ export class InjectedConnector implements VegaConnector {
}
async isAlive() {
return this.isConnected;
try {
const keys = await window.vega.listKeys();
if (keys.keys.length > 0) {
return true;
}
} catch (err) {
return false;
}
return false;
}
disconnect() {
clearInterval(this.alive);
clearConfig();
return window.vega.disconnectWallet();
}
@@ -130,7 +130,6 @@ interface ProposalNewMarketTerms {
decimalPlaces: string;
positionDecimalPlaces: string;
linearSlippageFactor: string;
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
quadraticSlippageFactor: string;
instrument: {
name: string;
@@ -168,7 +167,6 @@ interface ProposalUpdateMarketTerms {
marketId: string;
changes: {
linearSlippageFactor: string;
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
quadraticSlippageFactor: string;
instrument: {
code: string;
-3
View File
@@ -13,9 +13,6 @@ export interface VegaWalletContextShape {
/** Url of current connected node */
vegaUrl: string;
/** Vega chain id */
chainId: string;
/** Url of running wallet service */
vegaWalletServiceUrl: string;
-1
View File
@@ -7,4 +7,3 @@ export * from './provider';
export * from './connect-dialog';
export * from './utils';
export * from './storage';
export * from './use-chain-id';
-2
View File
@@ -22,7 +22,6 @@ const defaultConfig: VegaWalletConfig = {
chromeExtensionUrl: 'chrome-link',
mozillaExtensionUrl: 'mozilla-link',
},
chainId: 'VEGA_CHAIN_ID',
};
const setup = (config?: Partial<VegaWalletConfig>) => {
@@ -69,7 +68,6 @@ describe('VegaWalletProvider', () => {
expect(result.current).toEqual({
network: defaultConfig.network,
vegaUrl: defaultConfig.vegaUrl,
chainId: defaultConfig.chainId,
vegaWalletServiceUrl: defaultConfig.vegaWalletServiceUrl,
acknowledgeNeeded: false,
pubKey: null,
-2
View File
@@ -39,7 +39,6 @@ interface VegaWalletLinks {
export interface VegaWalletConfig {
network: Networks;
vegaUrl: string;
chainId: string;
vegaWalletServiceUrl: string;
links: VegaWalletLinks;
keepAlive?: number;
@@ -169,7 +168,6 @@ export const VegaWalletProvider = ({
const contextValue = useMemo<VegaWalletContextShape>(() => {
return {
vegaUrl: config.vegaUrl,
chainId: config.chainId,
vegaWalletServiceUrl: config.vegaWalletServiceUrl,
network: config.network,
links: {
+1 -2
View File
@@ -1,5 +1,6 @@
export function mockBrowserWallet(overrides?: Partial<Vega>) {
const vega: Vega = {
getChainId: jest.fn().mockReturnValue(Promise.resolve({ chainID: '1' })),
connectWallet: jest.fn().mockReturnValue(Promise.resolve(null)),
disconnectWallet: jest.fn().mockReturnValue(Promise.resolve()),
listKeys: jest
@@ -13,8 +14,6 @@ export function mockBrowserWallet(overrides?: Partial<Vega>) {
success: true,
txHash: '0x123',
}),
on: jest.fn(),
isConnected: jest.fn().mockRejectedValue(Promise.resolve(true)),
...overrides,
};
// @ts-ignore globalThis has no index signature
-100
View File
@@ -1,100 +0,0 @@
import { renderHook, waitFor } from '@testing-library/react';
import { MAX_FETCH_ATTEMPTS, useChainId } from './use-chain-id';
global.fetch = jest.fn();
const mockFetch = global.fetch as jest.Mock;
mockFetch.mockImplementation((url: string) => {
return Promise.resolve({ ok: true });
});
describe('useChainId', () => {
it('does not call fetch when statistics url could not be determined', async () => {
renderHook(() => useChainId(''));
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledTimes(0);
});
});
it('calls fetch with correct statistics url', async () => {
renderHook(() => useChainId('http://localhost:1234/graphql'));
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:1234/statistics'
);
});
});
it('does not return chain id when chain id is not present in response', async () => {
const { result } = renderHook(() =>
useChainId('http://localhost:1234/graphql')
);
await waitFor(() => {
expect(result.current).toBeUndefined();
});
});
it('returns chain id when chain id is present in response', async () => {
mockFetch.mockImplementation(() => {
return Promise.resolve({
ok: true,
json: () => ({
statistics: {
chainId: '1234',
},
}),
});
});
const { result } = renderHook(() =>
useChainId('http://localhost:1234/graphql')
);
await waitFor(() => {
expect(result.current).not.toBeUndefined();
expect(result.current).toEqual('1234');
});
});
it('returns chain id when within max number of attempts', async () => {
mockFetch.mockImplementation(() => {
if (mockFetch.mock.calls.length < MAX_FETCH_ATTEMPTS) {
return Promise.reject();
}
return Promise.resolve({
ok: true,
json: () => ({
statistics: {
chainId: '1234',
},
}),
});
});
const { result } = renderHook(() =>
useChainId('http://localhost:1234/graphql')
);
await waitFor(() => {
expect(result.current).not.toBeUndefined();
expect(result.current).toEqual('1234');
});
});
it('does not return chain id when max number of attempts exceeded', async () => {
mockFetch.mockImplementation(() => {
if (mockFetch.mock.calls.length < MAX_FETCH_ATTEMPTS + 10) {
return Promise.reject();
}
return Promise.resolve({
ok: true,
json: () => ({
statistics: {
chainId: '1234',
},
}),
});
});
const { result } = renderHook(() =>
useChainId('http://localhost:5678/graphql')
);
await waitFor(() => {
expect(result.current).toBeUndefined();
});
});
});
+3 -3
View File
@@ -7,7 +7,7 @@ import { useVegaWallet } from './use-vega-wallet';
export function useEagerConnect(connectors: Connectors) {
const [connecting, setConnecting] = useState(true);
const { vegaUrl, chainId, connect, acknowledgeNeeded } = useVegaWallet();
const { vegaUrl, connect, acknowledgeNeeded } = useVegaWallet();
useEffect(() => {
const attemptConnect = async () => {
@@ -33,7 +33,7 @@ export function useEagerConnect(connectors: Connectors) {
try {
if (connector instanceof InjectedConnector) {
await connector.connectWallet(chainId);
await connector.connectWallet();
await connect(connector);
} else if (connector instanceof SnapConnector) {
connector.nodeAddress = new URL(vegaUrl).origin;
@@ -51,7 +51,7 @@ export function useEagerConnect(connectors: Connectors) {
if (typeof window !== 'undefined') {
attemptConnect();
}
}, [connect, connectors, acknowledgeNeeded, vegaUrl, chainId]);
}, [connect, connectors, acknowledgeNeeded, vegaUrl]);
return connecting;
}
@@ -16,7 +16,6 @@ const defaultConfig: VegaWalletConfig = {
chromeExtensionUrl: 'chrome-link',
mozillaExtensionUrl: 'mozilla-link',
},
chainId: 'VEGA_CHAIN_ID',
};
const setup = (callback = jest.fn(), config?: Partial<VegaWalletConfig>) => {
@@ -47,10 +46,20 @@ describe('useInjectedConnector', () => {
expect(result.current.status).toBe(Status.Error);
});
it('errors if chain ids dont match', async () => {
mockBrowserWallet();
const { result } = setup();
await act(async () => {
result.current.connect(injected, '2'); // default mock chainId is '1'
});
expect(result.current.error?.message).toBe('Invalid chain');
expect(result.current.status).toBe(Status.Error);
});
it('errors if connection throws', async () => {
const callback = jest.fn();
mockBrowserWallet({
connectWallet: jest.fn().mockReturnValue(Promise.reject()),
getChainId: () => Promise.reject('failed'),
});
const { result } = setup(callback);
@@ -58,9 +67,7 @@ describe('useInjectedConnector', () => {
result.current.connect(injected, '1'); // default mock chainId is '1'
});
expect(result.current.status).toBe(Status.Error);
expect(result.current.error?.message).toBe(
'could not connect to the vega wallet on chain: 1'
);
expect(result.current.error?.message).toBe('injected connection failed');
});
it('connects', async () => {
@@ -71,6 +78,7 @@ describe('useInjectedConnector', () => {
act(() => {
result.current.connect(injected, '1'); // default mock chainId is '1'
});
expect(result.current.status).toBe(Status.GettingChainId);
await waitFor(() => {
expect(vega.connectWallet).toHaveBeenCalled();
+6 -8
View File
@@ -40,19 +40,17 @@ export const useInjectedConnector = (onConnect: () => void) => {
connector.nodeAddress = new URL(vegaUrl).origin;
}
// check the chain id for snap connector
if (connector instanceof SnapConnector) {
setStatus(Status.GettingChainId);
const { chainID } = await connector.getChainId();
if (chainID !== appChainId) {
throw InjectedConnectorErrors.INVALID_CHAIN;
}
setStatus(Status.GettingChainId);
const { chainID } = await connector.getChainId();
if (chainID !== appChainId) {
throw InjectedConnectorErrors.INVALID_CHAIN;
}
setStatus(Status.Connecting);
if (connector instanceof InjectedConnector) {
// extra step for injected connector - authorize wallet
await connector.connectWallet(appChainId);
await connector.connectWallet();
}
await connect(connector); // connect with keys
+8 -9
View File
@@ -1,11 +1,7 @@
export * from './lib/__generated__/TransactionResult';
export * from './lib/__generated__/WithdrawalApproval';
export * from './lib/constants';
export * from './lib/default-web3-provider';
export * from './lib/eip-1193-custom-bridge';
export * from './lib/ethereum-error';
export * from './lib/ethereum-transaction-dialog';
export * from './lib/types';
export * from './lib/url-connector';
export * from './lib/use-bridge-contract';
export * from './lib/use-eager-connect';
@@ -23,12 +19,7 @@ export * from './lib/use-get-withdraw-delay';
export * from './lib/use-get-withdraw-threshold';
export * from './lib/use-token-contract';
export * from './lib/use-token-decimals';
export * from './lib/use-transaction-result';
export * from './lib/use-vega-transaction-manager';
export * from './lib/use-vega-transaction-store';
export * from './lib/use-vega-transaction-toasts';
export * from './lib/use-vega-transaction-updater';
export * from './lib/use-wallet-disconnected-toasts';
export * from './lib/use-web3-disconnect';
export * from './lib/web3-connect-dialog';
export * from './lib/web3-connect-store';
@@ -36,3 +27,11 @@ export * from './lib/web3-connectors';
export * from './lib/web3-provider';
export * from './lib/withdrawal-approval-dialog';
export * from './lib/withdrawal-approval-status';
export * from './lib/default-web3-provider';
export * from './lib/use-vega-transaction-manager';
export * from './lib/use-vega-transaction-store';
export * from './lib/use-vega-transaction-updater';
export * from './lib/use-transaction-result';
export * from './lib/types';
export * from './lib/__generated__/TransactionResult';
export * from './lib/__generated__/WithdrawalApproval';
@@ -1,78 +0,0 @@
import {
Intent,
useToasts,
ToastHeading,
CLOSE_AFTER,
type Toast,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEffect, useMemo } from 'react';
import { useT } from './use-t';
import { usePrevious } from '@vegaprotocol/react-helpers';
export const WALLET_DISCONNECTED_TOAST_ID = 'WALLET_DISCONNECTED_TOAST_ID';
export const useWalletDisconnectToastActions = () => {
const [hasToast, updateToast] = useToasts((state) => [
state.hasToast,
state.update,
]);
const hideToast = () => {
if (!hasToast(WALLET_DISCONNECTED_TOAST_ID)) return;
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
hidden: true,
});
};
const showToast = () => {
if (!hasToast(WALLET_DISCONNECTED_TOAST_ID)) return;
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
hidden: false,
});
};
return { showToast, hideToast };
};
export const useWalletDisconnectedToasts = (
additionalContent?: JSX.Element
) => {
const t = useT();
const [hasToast, setToast] = useToasts((state) => [
state.hasToast,
state.setToast,
state.update,
]);
const { showToast, hideToast } = useWalletDisconnectToastActions();
const { isAlive } = useVegaWallet();
const wasAlive = usePrevious(isAlive);
const disconnected = wasAlive && !isAlive;
const toast: 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>
{additionalContent}
</>
),
onClose: () => {
hideToast();
},
closeAfter: CLOSE_AFTER,
}),
[additionalContent, hideToast, t]
);
useEffect(() => {
if (disconnected) {
if (hasToast(WALLET_DISCONNECTED_TOAST_ID)) {
showToast();
} else {
setToast(toast);
}
}
}, [disconnected, hasToast, isAlive, setToast, showToast, t, toast]);
};