Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7314f96d25 |
+1
-2
@@ -51,8 +51,7 @@
|
||||
"ul": ["list"],
|
||||
"ol": ["list"]
|
||||
}
|
||||
],
|
||||
"no-console": ["error", { "allow": ["warn", "error"] }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: Feature Epic
|
||||
title: 'Epic: '
|
||||
description: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
|
||||
labels: feature-epic
|
||||
---
|
||||
|
||||
## Core Feature
|
||||
|
||||
<Name>
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Define high level requirements
|
||||
- [ ] Create basic mockups
|
||||
- [ ] Update "API Requirements" in core spec
|
||||
- [ ] Update "User-Interface Spec" in relevant front end repo
|
||||
- [ ] Create detailed user stories using normal template
|
||||
|
||||
## High Level Requirements
|
||||
|
||||
## Basic Mockups
|
||||
|
||||
## Link to API Requirements in Core spec
|
||||
|
||||
## Link to User Interface Specs
|
||||
|
||||
## Linked User Stories
|
||||
@@ -29,12 +29,6 @@ jobs:
|
||||
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
|
||||
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
|
||||
echo IS_S3_RELEASE=false >> $GITHUB_ENV
|
||||
echo IS_DEV_IMAGE=false >> $GITHUB_ENV
|
||||
|
||||
- name: Is dev image
|
||||
if: ${{ contains(github.ref, 'develop') && github.event_name == 'push' && matrix.app == 'trading' }}
|
||||
run: |
|
||||
echo IS_DEV_IMAGE=true >> $GITHUB_ENV
|
||||
|
||||
- name: Is PR
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
@@ -81,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Log in to the Container registry (docker hub)
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -179,7 +173,7 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
continue-on-error: true
|
||||
id: dockerhub-push
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
|
||||
with:
|
||||
context: .
|
||||
file: docker/node-outside-docker.Dockerfile
|
||||
@@ -189,7 +183,7 @@ jobs:
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
||||
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || env.IS_DEV_IMAGE == 'true' && 'develop' || '' }}
|
||||
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }}
|
||||
|
||||
- name: Publish dist as docker image (ghcr - retry)
|
||||
uses: docker/build-push-action@v3
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSubContent,
|
||||
Icon,
|
||||
Button,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { FilterLabel } from './tx-filter-label';
|
||||
@@ -99,13 +100,15 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
|
||||
<DropdownMenu
|
||||
modal={false}
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
<FilterLabel filters={filters} />
|
||||
<DropdownMenuTrigger className="ml-0">
|
||||
<Button size="xs" data-testid="filter-trigger">
|
||||
<FilterLabel filters={filters} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{filters.size > 1 ? null : (
|
||||
{filters.size > 0 ? null : (
|
||||
<>
|
||||
<DropdownMenuCheckboxItem
|
||||
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const TxsListNavigation = ({
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
disabled={!hasMoreTxs}
|
||||
disabled={!hasMoreTxs || loading}
|
||||
onClick={() => {
|
||||
nextPage();
|
||||
}}
|
||||
|
||||
@@ -86,18 +86,14 @@ describe('Txs infinite list item', () => {
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<table>
|
||||
<tbody>
|
||||
<TxsInfiniteListItem
|
||||
type="testType"
|
||||
submitter="testPubKey"
|
||||
hash="testTxHash"
|
||||
block="1"
|
||||
code={0}
|
||||
command={{}}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<TxsInfiniteListItem
|
||||
type="testType"
|
||||
submitter="testPubKey"
|
||||
hash="testTxHash"
|
||||
block="1"
|
||||
code={0}
|
||||
command={{}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,9 @@ const DEFAULT_TRUNCATE_LENGTH = 7;
|
||||
|
||||
export function getIdTruncateLength(screen: Screen): number {
|
||||
if (['xxxl', 'xxl'].includes(screen)) {
|
||||
return 32;
|
||||
return 64;
|
||||
} else if (['xl', 'lg', 'md'].includes(screen)) {
|
||||
return 16;
|
||||
return 32;
|
||||
}
|
||||
return DEFAULT_TRUNCATE_LENGTH;
|
||||
}
|
||||
|
||||
@@ -5,17 +5,11 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
const generateHash = (): string =>
|
||||
Array.from(
|
||||
{ length: 64 },
|
||||
() => '0123456789ABCDEF'[Math.floor(Math.random() * 16)]
|
||||
).join('');
|
||||
|
||||
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||
return Array.from(Array(number)).map((_) => ({
|
||||
block: '87901',
|
||||
index: 2,
|
||||
hash: generateHash(),
|
||||
hash: '0F8B98DA0923A50786B852D9CA11E051CACC4C733E1DB93D535C7D81DBD10F6F',
|
||||
submitter:
|
||||
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
|
||||
type: 'Submit Order',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Table, TableRow } from '../table';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
||||
import { getTxsDataUrl } from '../../hooks/get-txs-data-url';
|
||||
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import EmptyList from '../empty-list/empty-list';
|
||||
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||
@@ -14,7 +14,7 @@ interface TxsPerBlockProps {
|
||||
|
||||
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
||||
const filters = `filters[block.height]=${blockHeight}`;
|
||||
const url = getTxsDataUrl({ filters, count: txCount });
|
||||
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
|
||||
const {
|
||||
state: { data, loading, error },
|
||||
} = useFetch<BlockExplorerTransactions>(url);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { DATA_SOURCES } from '../config';
|
||||
|
||||
type IGetTxsDataFirstPage = {
|
||||
baseUrl?: string;
|
||||
count?: number;
|
||||
party?: string;
|
||||
filters?: string;
|
||||
};
|
||||
|
||||
interface IGetTxsDataPrevious extends IGetTxsDataFirstPage {
|
||||
before: string;
|
||||
}
|
||||
|
||||
interface IGetTxsDataNext extends IGetTxsDataFirstPage {
|
||||
after: string;
|
||||
}
|
||||
|
||||
type IGetTxsDataUrl =
|
||||
| IGetTxsDataPrevious
|
||||
| IGetTxsDataNext
|
||||
| IGetTxsDataFirstPage;
|
||||
|
||||
export const BE_TXS_PER_REQUEST = 25;
|
||||
|
||||
/**
|
||||
* Properly encodes the filters and parameters for a request to the block explorer
|
||||
* API for transactions. As the API uses a slightly less common format for encoding
|
||||
* filters, some of it is more manual than you might expect.
|
||||
*
|
||||
* @param params An object containing the pagination and filters
|
||||
* @returns string URL to call
|
||||
*/
|
||||
export const getTxsDataUrl = (params: IGetTxsDataUrl) => {
|
||||
const baseUrl =
|
||||
params.baseUrl || `${DATA_SOURCES.blockExplorerUrl}/transactions`;
|
||||
const url = new URL(baseUrl);
|
||||
const count = `${params.count || BE_TXS_PER_REQUEST}`;
|
||||
|
||||
if ('before' in params && params.before?.length > 0) {
|
||||
url.searchParams.append('last', count);
|
||||
url.searchParams.append('before', params.before);
|
||||
} else if ('after' in params && params.after?.length > 0) {
|
||||
url.searchParams.append('first', count);
|
||||
url.searchParams.append('after', params.after);
|
||||
} else {
|
||||
url.searchParams.append('first', count);
|
||||
}
|
||||
|
||||
// Hacky fix for param as array
|
||||
let urlAsString = url.toString();
|
||||
if (params.filters && params.filters?.length > 0) {
|
||||
urlAsString += '&' + params.filters.replaceAll(' ', '%20');
|
||||
}
|
||||
if (params.party && params.party?.length > 0) {
|
||||
urlAsString += `&filters[tx.submitter]=${params.party}`;
|
||||
}
|
||||
|
||||
return urlAsString;
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { getTxsDataUrl } from './get-txs-data-url'; // import the function to be tested
|
||||
|
||||
describe('getTxsDataUrl', () => {
|
||||
it('should return the correct URL without filters and party', () => {
|
||||
const params = {
|
||||
count: 10,
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl = 'https://example.com/transactions?first=10';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return the correct URL with "before" in params', () => {
|
||||
const params = {
|
||||
count: 5,
|
||||
before: '100.1',
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl = 'https://example.com/transactions?last=5&before=100.1';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return the correct URL with "after" in params', () => {
|
||||
const params = {
|
||||
count: 5,
|
||||
after: '222.1',
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl = 'https://example.com/transactions?first=5&after=222.1';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return the correct URL with filters and party', () => {
|
||||
const params = {
|
||||
count: 10,
|
||||
filters: 'filters[cmd.type]=Made Up Transaction',
|
||||
party: '1234',
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl =
|
||||
'https://example.com/transactions?first=10&filters[cmd.type]=Made%20Up%20Transaction&filters[tx.submitter]=1234';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
});
|
||||
@@ -1,141 +1,130 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type { URLSearchParamsInit } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
BlockExplorerTransactionResult,
|
||||
BlockExplorerTransactions,
|
||||
} from '../routes/types/block-explorer-response';
|
||||
import { DATA_SOURCES } from '../config';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import { AllFilterOptions } from '../components/txs/tx-filter';
|
||||
import type { FilterOption } from '../components/txs/tx-filter';
|
||||
import { BE_TXS_PER_REQUEST, getTxsDataUrl } from './get-txs-data-url';
|
||||
|
||||
export function getTypeFilters(filters?: Set<FilterOption>) {
|
||||
if (!filters) {
|
||||
return '';
|
||||
} else if (filters.size > 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const forcedSingleFilter = Array.from(filters)[0];
|
||||
return `filters[cmd.type]=${forcedSingleFilter}`;
|
||||
export interface TxsStateProps {
|
||||
txsData: BlockExplorerTransactionResult[];
|
||||
hasMoreTxs: boolean;
|
||||
cursor: string;
|
||||
previousCursors: string[];
|
||||
hasPreviousPage: boolean;
|
||||
}
|
||||
|
||||
export interface IUseTxsData {
|
||||
count?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
party?: string;
|
||||
filters?: Set<FilterOption>;
|
||||
limit: number;
|
||||
filters?: string;
|
||||
}
|
||||
|
||||
export const useTxsData = ({
|
||||
count = 25,
|
||||
before,
|
||||
after,
|
||||
filters,
|
||||
party,
|
||||
}: IUseTxsData) => {
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
let hasMoreTxs = true;
|
||||
let txsData: BlockExplorerTransactionResult[] = [];
|
||||
interface IGetTxsDataUrl {
|
||||
limit: string;
|
||||
filters?: string;
|
||||
}
|
||||
|
||||
const url = getTxsDataUrl({
|
||||
filters: getTypeFilters(filters),
|
||||
count,
|
||||
before,
|
||||
after,
|
||||
party,
|
||||
export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
|
||||
const url = new URL(`${DATA_SOURCES.blockExplorerUrl}/transactions`);
|
||||
|
||||
if (limit) {
|
||||
url.searchParams.append('limit', limit);
|
||||
}
|
||||
|
||||
// Hacky fix for param as array
|
||||
let urlAsString = url.toString();
|
||||
if (filters) {
|
||||
urlAsString += '&' + filters.replace(' ', '%20');
|
||||
}
|
||||
|
||||
return urlAsString;
|
||||
};
|
||||
|
||||
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
||||
const [
|
||||
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage },
|
||||
setTxsState,
|
||||
] = useState<TxsStateProps>({
|
||||
txsData: [],
|
||||
hasMoreTxs: false,
|
||||
previousCursors: [],
|
||||
cursor: '',
|
||||
hasPreviousPage: false,
|
||||
});
|
||||
|
||||
const url = getTxsDataUrl({ limit: limit.toString(), filters });
|
||||
|
||||
const {
|
||||
state: { data, error, loading },
|
||||
refetch,
|
||||
} = useFetch<BlockExplorerTransactions>(url, {}, true);
|
||||
|
||||
if (!loading && data && isNumber(data.transactions.length)) {
|
||||
hasMoreTxs = data.transactions.length >= count;
|
||||
txsData = data.transactions;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!loading && data && isNumber(data.transactions.length)) {
|
||||
setTxsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
txsData: data.transactions,
|
||||
hasMoreTxs: data.transactions.length >= limit,
|
||||
cursor: data?.transactions.at(-1)?.cursor || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [loading, setTxsState, data, limit]);
|
||||
|
||||
const nextPage = useCallback(() => {
|
||||
const after = data?.transactions.at(-1)?.cursor || '';
|
||||
const params: URLSearchParamsInit = { after };
|
||||
if (filters) {
|
||||
params.filters = Array.from(filters).join(',');
|
||||
}
|
||||
setSearchParams(params);
|
||||
}, [filters, data, setSearchParams]);
|
||||
const c = data?.transactions.at(0)?.cursor;
|
||||
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
|
||||
|
||||
setTxsState((prev) => ({
|
||||
...prev,
|
||||
hasPreviousPage: true,
|
||||
previousCursors: newPreviousCursors,
|
||||
}));
|
||||
|
||||
return refetch({
|
||||
limit,
|
||||
before: cursor,
|
||||
});
|
||||
}, [data, previousCursors, cursor, limit, refetch]);
|
||||
|
||||
const previousPage = useCallback(() => {
|
||||
const before = data?.transactions[0]?.cursor || '';
|
||||
const params: URLSearchParamsInit = { before };
|
||||
if (filters && filters.size > 0 && filters.size === 1) {
|
||||
params.filters = Array.from(filters)[0];
|
||||
}
|
||||
setSearchParams(params);
|
||||
}, [filters, data, setSearchParams]);
|
||||
const previousCursor = [...previousCursors].pop();
|
||||
const newPreviousCursors = previousCursors.slice(0, -1);
|
||||
setTxsState((prev) => ({
|
||||
...prev,
|
||||
hasPreviousPage: newPreviousCursors.length > 0,
|
||||
previousCursors: newPreviousCursors,
|
||||
}));
|
||||
return refetch({
|
||||
limit,
|
||||
before: previousCursor,
|
||||
});
|
||||
}, [previousCursors, limit, refetch]);
|
||||
|
||||
const refreshTxs = useCallback(async () => {
|
||||
const params: URLSearchParamsInit = {};
|
||||
if (filters && filters.size > 0 && filters.size === 1) {
|
||||
params.filters = Array.from(filters)[0];
|
||||
}
|
||||
setSearchParams(params);
|
||||
setTxsState(() => ({
|
||||
txsData: [],
|
||||
cursor: '',
|
||||
previousCursors: [],
|
||||
hasMoreTxs: false,
|
||||
hasPreviousPage: false,
|
||||
}));
|
||||
|
||||
refetch({ count: BE_TXS_PER_REQUEST });
|
||||
}, [setSearchParams, refetch, filters]);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
(newFilters: Set<FilterOption>) => {
|
||||
const params: URLSearchParamsInit = {};
|
||||
if (newFilters && newFilters.size === 1) {
|
||||
params.filters = Array.from(newFilters)[0];
|
||||
}
|
||||
|
||||
setSearchParams(params);
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
refetch({ limit });
|
||||
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return {
|
||||
updateFilters,
|
||||
txsData,
|
||||
loading,
|
||||
error,
|
||||
hasMoreTxs,
|
||||
hasPreviousPage,
|
||||
previousCursors,
|
||||
cursor,
|
||||
refreshTxs,
|
||||
nextPage,
|
||||
previousPage,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a Set of filters based on the URLSearchParams, or
|
||||
* defaults to all.
|
||||
* @param params
|
||||
* @returns Set
|
||||
*/
|
||||
export function getInitialFilters(params: URLSearchParams): Set<FilterOption> {
|
||||
const defaultFilters = new Set(AllFilterOptions);
|
||||
|
||||
const p = params.get('filters');
|
||||
|
||||
if (!p) {
|
||||
return defaultFilters;
|
||||
}
|
||||
|
||||
const filters = new Set<FilterOption>();
|
||||
p.split(',').forEach((f) => {
|
||||
if (AllFilterOptions.includes(f as FilterOption)) {
|
||||
filters.add(f as FilterOption);
|
||||
}
|
||||
});
|
||||
|
||||
if (filters.size === 0) {
|
||||
return defaultFilters;
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
@@ -204,7 +204,6 @@ describe('Block', () => {
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: createBlockResponse(1), loading: false, error: null },
|
||||
});
|
||||
|
||||
render(renderComponent(1));
|
||||
await waitFor(() => screen.getByTestId('block-header'));
|
||||
expect(screen.getByTestId('previous-block-button')).toHaveAttribute(
|
||||
|
||||
@@ -15,13 +15,9 @@ import { PartyBlockAccounts } from './components/party-block-accounts';
|
||||
import { isValidPartyId } from './components/party-id-error';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||
import type { FilterOption } from '../../../components/txs/tx-filter';
|
||||
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const Party = () => {
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||
const { party } = useParams<{ party: string }>();
|
||||
|
||||
@@ -31,21 +27,24 @@ const Party = () => {
|
||||
const partyId = toNonHex(party ? party : '');
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
||||
const baseFilters = `filters[tx.submitter]=${partyId}`;
|
||||
const f =
|
||||
filters && filters.size === 1
|
||||
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}`
|
||||
: baseFilters;
|
||||
|
||||
const {
|
||||
hasMoreTxs,
|
||||
nextPage,
|
||||
refreshTxs,
|
||||
previousPage,
|
||||
error,
|
||||
refreshTxs,
|
||||
loading,
|
||||
txsData,
|
||||
hasMoreTxs,
|
||||
updateFilters,
|
||||
hasPreviousPage,
|
||||
} = useTxsData({
|
||||
filters: filters.size === 1 ? filters : undefined,
|
||||
before: params.get('before') || undefined,
|
||||
after: !params.get('before') ? params.get('after') || undefined : undefined,
|
||||
party: partyId,
|
||||
limit: 25,
|
||||
filters: f,
|
||||
});
|
||||
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
@@ -103,21 +102,15 @@ const Party = () => {
|
||||
refreshTxs={refreshTxs}
|
||||
nextPage={nextPage}
|
||||
previousPage={previousPage}
|
||||
hasPreviousPage={true}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
loading={loading}
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
>
|
||||
<TxsFilter
|
||||
filters={filters}
|
||||
setFilters={(f) => {
|
||||
setFilters(f);
|
||||
updateFilters(f as Set<FilterOption>);
|
||||
}}
|
||||
/>
|
||||
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||
</TxsListNavigation>
|
||||
{!error && txsData ? (
|
||||
<TxsInfiniteList
|
||||
hasMoreTxs={true}
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
areTxsLoading={loading}
|
||||
txs={txsData}
|
||||
loadMoreTxs={nextPage}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { TxsInfiniteList } from '../../../components/txs';
|
||||
import { useTxsData, getInitialFilters } from '../../../hooks/use-txs-data';
|
||||
import { useTxsData } from '../../../hooks/use-txs-data';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { TxsFilter } from '../../../components/txs/tx-filter';
|
||||
import type { FilterOption } from '../../../components/txs/tx-filter';
|
||||
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const BE_TXS_PER_REQUEST = 25;
|
||||
|
||||
export const TxsList = () => {
|
||||
useDocumentTitle(['Transactions']);
|
||||
@@ -27,22 +27,25 @@ export const TxsList = () => {
|
||||
* @returns {JSX.Element} Transaction List and controls
|
||||
*/
|
||||
export const TxsListFiltered = () => {
|
||||
const [params] = useSearchParams();
|
||||
const [filters, setFilters] = useState(getInitialFilters(params));
|
||||
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||
|
||||
const f =
|
||||
filters && filters.size === 1
|
||||
? `filters[cmd.type]=${Array.from(filters)[0]}`
|
||||
: '';
|
||||
|
||||
const {
|
||||
hasMoreTxs,
|
||||
nextPage,
|
||||
previousPage,
|
||||
error,
|
||||
refreshTxs,
|
||||
loading,
|
||||
txsData,
|
||||
hasMoreTxs,
|
||||
updateFilters,
|
||||
hasPreviousPage,
|
||||
} = useTxsData({
|
||||
filters: filters.size === 1 ? filters : undefined,
|
||||
before: params.get('before') || undefined,
|
||||
after: !params.get('before') ? params.get('after') || undefined : undefined,
|
||||
limit: BE_TXS_PER_REQUEST,
|
||||
filters: f,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -51,17 +54,11 @@ export const TxsListFiltered = () => {
|
||||
refreshTxs={refreshTxs}
|
||||
nextPage={nextPage}
|
||||
previousPage={previousPage}
|
||||
hasPreviousPage={true}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
loading={loading}
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
>
|
||||
<TxsFilter
|
||||
filters={filters}
|
||||
setFilters={(f) => {
|
||||
setFilters(f);
|
||||
updateFilters(f as Set<FilterOption>);
|
||||
}}
|
||||
/>
|
||||
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||
</TxsListNavigation>
|
||||
<TxsInfiniteList
|
||||
hasFilters={filters.size > 0}
|
||||
|
||||
@@ -16,7 +16,6 @@ NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
@@ -26,4 +25,4 @@ CYPRESS_FAIRGROUND=false
|
||||
LC_ALL="en_US.UTF-8"
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
|
||||
@@ -17,7 +17,6 @@ NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=http://localhost:26617
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||
@@ -26,4 +25,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
|
||||
@@ -11,11 +11,10 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
|
||||
@@ -13,10 +13,9 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
|
||||
@@ -12,10 +12,9 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
|
||||
@@ -8,10 +8,9 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
|
||||
@@ -12,11 +12,10 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
|
||||
@@ -9,11 +9,10 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
|
||||
@@ -18,7 +18,10 @@ import { VegaWallet } from '../vega-wallet';
|
||||
import { useLocation, useMatch } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog';
|
||||
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
ProtocolUpgradeCountdown,
|
||||
ProtocolUpgradeCountdownMode,
|
||||
} from '@vegaprotocol/proposals';
|
||||
|
||||
export const SettingsLink = () => {
|
||||
const { open, isOpen, close } = useTelemetryDialog();
|
||||
@@ -65,7 +68,9 @@ export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
|
||||
actions={
|
||||
<>
|
||||
<SettingsLink />
|
||||
<ProtocolUpgradeCountdown />
|
||||
<ProtocolUpgradeCountdown
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -66,7 +66,6 @@ export const ENV = {
|
||||
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
|
||||
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
|
||||
delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'),
|
||||
rest: windowOrDefault('NX_VEGA_REST_URL'),
|
||||
addresses:
|
||||
ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks],
|
||||
};
|
||||
|
||||
@@ -20,6 +20,6 @@ export const downloadJson = (jsonString: string, proposalTitle: string) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Proposal } from '../components/proposal';
|
||||
import { ProposalNotFound } from '../components/proposal-not-found';
|
||||
import { useProposalQuery } from './__generated__/Proposal';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { ENV } from '../../../config';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
|
||||
import { useAssetQuery } from '@vegaprotocol/assets';
|
||||
@@ -14,8 +13,11 @@ import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
export const ProposalContainer = () => {
|
||||
const { VEGA_REST_URL } = useEnvironment();
|
||||
const REST_ENDPOINT = VEGA_REST_URL;
|
||||
const [
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
setMostRecentlyEnactedAssociatedMarketProposal,
|
||||
@@ -45,7 +47,7 @@ export const ProposalContainer = () => {
|
||||
|
||||
const {
|
||||
state: { data: restData, loading: restLoading, error: restError },
|
||||
} = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
|
||||
} = useFetch(`${REST_ENDPOINT}governance?proposalId=${params.proposalId}`);
|
||||
|
||||
const { data, loading, error, refetch } = useProposalQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
@@ -61,7 +63,7 @@ export const ProposalContainer = () => {
|
||||
error: originalMarketProposalRestError,
|
||||
},
|
||||
} = useFetch(
|
||||
`${ENV.rest}governance?proposalId=${
|
||||
`${REST_ENDPOINT}governance?proposalId=${
|
||||
data?.proposal?.terms.change.__typename === 'UpdateMarket' &&
|
||||
data?.proposal.terms.change.marketId
|
||||
}`,
|
||||
@@ -77,7 +79,7 @@ export const ProposalContainer = () => {
|
||||
error: previouslyEnactedMarketProposalsRestError,
|
||||
},
|
||||
} = useFetch(
|
||||
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
|
||||
`${REST_ENDPOINT}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
|
||||
undefined,
|
||||
true,
|
||||
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
|
||||
|
||||
+15
@@ -5,6 +5,7 @@ import {
|
||||
RewardsTable,
|
||||
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
|
||||
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface EpochIndividualRewardsGridProps {
|
||||
data: EpochIndividualReward;
|
||||
@@ -21,11 +22,14 @@ interface RewardItemProps {
|
||||
const DisplayReward = ({
|
||||
reward,
|
||||
decimals,
|
||||
percentageOfTotal,
|
||||
}: {
|
||||
reward: string;
|
||||
decimals: number;
|
||||
percentageOfTotal?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (Number(reward) === 0) {
|
||||
return <span className="text-vega-dark-300">-</span>;
|
||||
}
|
||||
@@ -35,12 +39,23 @@ const DisplayReward = ({
|
||||
description={
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
|
||||
{percentageOfTotal && (
|
||||
<span className="text-vega-dark-300">
|
||||
({percentageOfTotal}% {t('ofTotalDistributed')})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button>
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{formatNumber(toBigNum(reward, decimals), 4)}</span>
|
||||
{percentageOfTotal && (
|
||||
<span className="text-vega-dark-300">
|
||||
({formatNumber(percentageOfTotal, 4).toString()}
|
||||
%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
+76
@@ -168,6 +168,17 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
expect(result2[1].epoch).toEqual(1);
|
||||
});
|
||||
|
||||
it('correctly calculates the total value of rewards for an asset', () => {
|
||||
const rewards = [reward1, reward4];
|
||||
const result = generateEpochIndividualRewardsList({
|
||||
rewards,
|
||||
epochId: 1,
|
||||
epochRewardSummaries: [],
|
||||
});
|
||||
|
||||
expect(result[0].rewards[0].totalAmount).toEqual('200');
|
||||
});
|
||||
|
||||
it('returns data in the expected shape', () => {
|
||||
// Just sanity checking the whole structure here
|
||||
const rewards = [reward1, reward2, reward3, reward4];
|
||||
@@ -447,4 +458,69 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('correctly calculates the percentage of two or more rewards by referencing the total rewards amount', () => {
|
||||
const result = generateEpochIndividualRewardsList({
|
||||
rewards: [
|
||||
// reward1 is 100 usd, which is 10% of the total rewards amount
|
||||
reward1,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
amount: '200',
|
||||
percentageOfTotal: '0.2',
|
||||
receivedAt: new Date(),
|
||||
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
|
||||
party: { id: 'blah' },
|
||||
epoch: { id: '1' },
|
||||
},
|
||||
],
|
||||
epochId: 1,
|
||||
epochRewardSummaries: [
|
||||
{
|
||||
__typename: 'EpochRewardSummary',
|
||||
epoch: 1,
|
||||
assetId: 'usd',
|
||||
amount: '1000',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
epoch: 1,
|
||||
rewards: [
|
||||
{
|
||||
asset: 'USD',
|
||||
decimals: 6,
|
||||
totalAmount: '300',
|
||||
rewardTypes: {
|
||||
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
||||
amount: '300',
|
||||
percentageOfTotal: '30',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+16
-1
@@ -59,6 +59,7 @@ export const generateEpochIndividualRewardsList = ({
|
||||
const epochIndividualRewards = rewards.reduce((acc, reward) => {
|
||||
const epochId = reward.epoch.id;
|
||||
const assetName = reward.asset.name;
|
||||
const assetId = reward.asset.id;
|
||||
const assetDecimals = reward.asset.decimals;
|
||||
const rewardType = reward.rewardType;
|
||||
const amount = reward.amount;
|
||||
@@ -76,6 +77,13 @@ export const generateEpochIndividualRewardsList = ({
|
||||
const epoch = acc.get(epochId);
|
||||
|
||||
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
|
||||
const matchingTotalRewardAmount = epochRewardSummaries.find(
|
||||
(summary) =>
|
||||
summary.epoch === Number(epochId) &&
|
||||
summary.assetId === assetId &&
|
||||
summary.rewardType === rewardType
|
||||
)?.amount;
|
||||
|
||||
let asset = epoch?.rewards.find((r) => r.asset === assetName);
|
||||
|
||||
if (!asset) {
|
||||
@@ -98,7 +106,14 @@ export const generateEpochIndividualRewardsList = ({
|
||||
|
||||
asset.rewardTypes[rewardType] = {
|
||||
amount: newAmount,
|
||||
percentageOfTotal: percentageOfTotal,
|
||||
percentageOfTotal: matchingTotalRewardAmount
|
||||
? new BigNumber(newAmount)
|
||||
.dividedBy(matchingTotalRewardAmount)
|
||||
.multipliedBy(100)
|
||||
.toString()
|
||||
: // this should never be reached, if there's an individual reward there should
|
||||
// always be a reward total from the api too, but set it as a fallback just in case
|
||||
percentageOfTotal,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ export const RewardsPage = () => {
|
||||
error: paramsError,
|
||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||
|
||||
console.log('params', params);
|
||||
|
||||
const payoutDuration = useMemo(() => {
|
||||
if (!params) {
|
||||
return 0;
|
||||
|
||||
@@ -535,7 +535,6 @@ function checkIfDataAndTimeOfCreationAndUpdateIsEqual(date: string) {
|
||||
// unexpected latency
|
||||
const minBefore = subSeconds(new Date(), 5);
|
||||
const maxAfter = addSeconds(new Date(), 5);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(maxAfter);
|
||||
const date = new Date($dateTime.toString());
|
||||
expect(isAfter(date, minBefore) && isBefore(date, maxAfter)).to.equal(
|
||||
|
||||
@@ -68,4 +68,22 @@ describe('home', { tags: '@regression' }, () => {
|
||||
cy.getByTestId('connect').click();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Network switcher', () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('switch to fairground network and check status & incidents link', () => {
|
||||
// 0006-NETW-002
|
||||
// 0006-NETW-003
|
||||
cy.getByTestId('navigation')
|
||||
.find('[data-testid="network-switcher"]')
|
||||
.should('have.text', 'Custom')
|
||||
.click();
|
||||
cy.getByTestId('network-item').contains('Fairground testnet');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
.find('button')
|
||||
.click();
|
||||
|
||||
const dropdownContent = '[data-testid="proposal-actions-content"]';
|
||||
const dropdownContent = '[data-testid="market-actions-content"]';
|
||||
const dropdownContentItem = '[role="menuitem"]';
|
||||
|
||||
// 6001-MARK-059
|
||||
@@ -100,6 +100,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
'VEGA_TOKEN_URL'
|
||||
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
|
||||
);
|
||||
cy.getByTestId('market-actions-content').click();
|
||||
});
|
||||
|
||||
// 6001-MARK-060
|
||||
@@ -213,12 +214,11 @@ describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
|
||||
aliasGQLQuery(req, 'ProposalsList', proposal);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Proposed markets"]').click();
|
||||
});
|
||||
|
||||
it('can see no markets message', () => {
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Proposed markets"]').click();
|
||||
|
||||
// 6001-MARK-061
|
||||
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
|
||||
describe('Navbar', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.clearAllLocalStorage();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
});
|
||||
|
||||
const pages = [
|
||||
{ name: 'Markets', link: '#/markets/all' },
|
||||
{ name: 'Trading', link: '#/markets' },
|
||||
{ name: 'Portfolio', link: '#/portfolio' },
|
||||
];
|
||||
|
||||
describe('desktop view', () => {
|
||||
pages.forEach(({ name, link }) => {
|
||||
it(`${name} should be correctly rendered`, () => {
|
||||
cy.get('nav')
|
||||
.find(`a[data-testid=${name}]:visible`)
|
||||
.then((element) => {
|
||||
cy.wrap(element).click();
|
||||
cy.location('hash').should('contain', link);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Resources dropdown should be correctly rendered', () => {
|
||||
const resourceSelector = 'ul li:contains(Resources)';
|
||||
['Docs', 'Give Feedback'].forEach((text, index) => {
|
||||
cy.get('nav').find(resourceSelector).contains('Resources').click();
|
||||
cy.get('nav')
|
||||
.find(resourceSelector)
|
||||
.find('.navigation-content li')
|
||||
.eq(index)
|
||||
.find('a')
|
||||
.then((element) => {
|
||||
expect(element.attr('target')).to.eq('_blank');
|
||||
expect(element.attr('href')).to.not.be.empty;
|
||||
expect(element.text()).to.eq(text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Disclaimer should be presented after choosing from menu', () => {
|
||||
cy.get('nav')
|
||||
.find('ul li:contains(Resources)')
|
||||
.contains('Resources')
|
||||
.click();
|
||||
cy.getByTestId('Disclaimer').eq(0).click();
|
||||
cy.location('hash').should('equal', '#/disclaimer');
|
||||
cy.get('p').contains(
|
||||
'Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mobile view', () => {
|
||||
const viewportHeight = Cypress.config('viewportHeight');
|
||||
const viewportWidth = Cypress.config('viewportWidth');
|
||||
before(() => {
|
||||
// a little hack to keep the viewport size between tests (cypress bug)
|
||||
Cypress.config({
|
||||
viewportWidth: 560,
|
||||
viewportHeight: 890,
|
||||
});
|
||||
cy.viewport(560, 890);
|
||||
});
|
||||
|
||||
describe('wallet drawer', () => {
|
||||
it('wallet drawer should be correctly rendered', () => {
|
||||
mockConnectWallet();
|
||||
cy.connectVegaWallet(true);
|
||||
cy.getByTestId('connect-vega-wallet-mobile').click();
|
||||
cy.getByTestId('wallets-drawer').should('be.visible');
|
||||
cy.getByTestId('wallets-drawer').within((el) => {
|
||||
cy.wrap(el).get('button').contains('Disconnect').click();
|
||||
});
|
||||
cy.getByTestId('wallets-drawer').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('menu drawer', () => {
|
||||
pages.forEach(({ name, link }) => {
|
||||
it(`${name} should be correctly rendered`, () => {
|
||||
cy.getByTestId('button-menu-drawer').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
cy.getByTestId('menu-drawer').within((el) => {
|
||||
cy.wrap(el).getByTestId(name).click();
|
||||
cy.location('hash').should('contain', link);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Menu drawer should not be visible until opened', () => {
|
||||
cy.getByTestId('menu-drawer').should('not.be.visible');
|
||||
cy.getByTestId('button-menu-drawer').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
cy.getByTestId('button-menu-drawer').click();
|
||||
cy.getByTestId('menu-drawer').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
after(() => {
|
||||
// a little hack to keep the viewport size between tests (cypress bug)
|
||||
Cypress.config({
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -171,7 +171,6 @@ describe(
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
const actualDate = text.slice(0, -67);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(actualDate);
|
||||
const actualOhlc = text.slice(-67);
|
||||
assert.isTrue(expectedDateRegex.test(actualDate));
|
||||
|
||||
@@ -28,16 +28,16 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
|
||||
|
||||
it('must be able to select order direction - long/short', function () {
|
||||
// 7002-SORD-004
|
||||
cy.getByTestId(toggleShort).click().next('input').should('be.checked');
|
||||
cy.getByTestId(toggleLong).click().next('input').should('be.checked');
|
||||
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
|
||||
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
|
||||
});
|
||||
|
||||
it('must be able to select order type - limit/market', function () {
|
||||
// 7002-SORD-005
|
||||
// 7002-SORD-006
|
||||
// 7002-SORD-007
|
||||
cy.getByTestId(toggleLimit).click().next('input').should('be.checked');
|
||||
cy.getByTestId(toggleMarket).click().next('input').should('be.checked');
|
||||
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
|
||||
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
|
||||
});
|
||||
|
||||
it('order connect vega wallet button should connect', () => {
|
||||
@@ -51,7 +51,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(placeOrderBtn).should('be.visible');
|
||||
cy.getByTestId(toggleLimit).next('input').should('be.checked');
|
||||
cy.getByTestId(toggleLimit).children('input').should('be.checked');
|
||||
cy.getByTestId(orderPriceField).should('have.value', '101');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ const closePosition = 'close-position';
|
||||
const dialogCloseX = 'dialog-close';
|
||||
const dialogContent = 'dialog-content';
|
||||
const dropDownMenu = 'dropdown-menu';
|
||||
const marketActionsContent = 'position-actions-content';
|
||||
const marketActionsContent = 'market-actions-content';
|
||||
const positions = 'Positions';
|
||||
const tabPositions = 'tab-positions';
|
||||
const toastContent = 'toast-content';
|
||||
|
||||
@@ -35,8 +35,6 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
|
||||
cy.setVegaWallet();
|
||||
|
||||
cy.visit('/');
|
||||
cy.getByTestId(manageVegaWallet).click();
|
||||
cy.getByTestId(walletTransfer).click();
|
||||
|
||||
cy.wait('@Assets');
|
||||
cy.wait('@Accounts');
|
||||
@@ -59,6 +57,7 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
|
||||
// 1003-TRAN-019
|
||||
cy.getByTestId(transferForm);
|
||||
cy.contains('Enter manually').click();
|
||||
|
||||
cy.getByTestId(transferForm)
|
||||
.find(toAddressField)
|
||||
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
|
||||
const orderSizeField = 'order-size';
|
||||
@@ -10,9 +9,7 @@ export const createOrder = (order: OrderSubmission): void => {
|
||||
cy.log('Placing order', order);
|
||||
const { type, side, size, price, timeInForce, expiresAt } = order;
|
||||
|
||||
cy.getByTestId(
|
||||
`order-type-${type === OrderType.TYPE_LIMIT ? 'Limit' : 'Market'}`
|
||||
).click();
|
||||
cy.getByTestId(`order-type-${type}`).click();
|
||||
cy.getByTestId(`order-side-${side}`).click();
|
||||
cy.getByTestId(orderSizeField).clear().type(size);
|
||||
if (price) {
|
||||
|
||||
@@ -6,8 +6,8 @@ export const orderTIFDropDown = 'order-tif';
|
||||
export const placeOrderBtn = 'place-order';
|
||||
export const toggleShort = 'order-side-SIDE_SELL';
|
||||
export const toggleLong = 'order-side-SIDE_BUY';
|
||||
export const toggleLimit = 'order-type-Limit';
|
||||
export const toggleMarket = 'order-type-Market';
|
||||
export const toggleLimit = 'order-type-TYPE_LIMIT';
|
||||
export const toggleMarket = 'order-type-TYPE_MARKET';
|
||||
|
||||
export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
||||
return {
|
||||
|
||||
+1
-1
@@ -16,6 +16,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -18,6 +18,6 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_STOP_ORDERS=false
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.21-core-0.71.6
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_STOP_ORDERS=false
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_STOP_ORDERS=false
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_STOP_ORDERS=false
|
||||
# NX_STOP_ORDERS
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
@@ -58,11 +58,7 @@ const MainGrid = memo(
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs storageKey="console-trade-grid-main-left">
|
||||
<Tab
|
||||
id="chart"
|
||||
name={t('Chart')}
|
||||
menu={<TradingViews.candles.menu />}
|
||||
>
|
||||
<Tab id="chart" name={t('Chart')}>
|
||||
<TradingViews.candles.component marketId={marketId} />
|
||||
</Tab>
|
||||
<Tab id="depth" name={t('Depth')}>
|
||||
@@ -134,13 +130,6 @@ const MainGrid = memo(
|
||||
<TradingViews.orders.component marketId={marketId} />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
{FLAGS.STOP_ORDERS ? (
|
||||
<Tab id="stop-orders" name={t('Stop orders')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.stopOrders.component />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
) : null}
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.fills.component
|
||||
|
||||
@@ -37,7 +37,6 @@ export const TradePanels = ({
|
||||
const onOrderTypeClick = useMarketLiquidityClickHandler();
|
||||
|
||||
const [view, setView] = useState<TradingView>('candles');
|
||||
|
||||
const renderView = () => {
|
||||
const Component = memo<{
|
||||
marketId: string;
|
||||
@@ -66,23 +65,8 @@ export const TradePanels = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenu = () => {
|
||||
const viewCfg = TradingViews[view];
|
||||
|
||||
if ('menu' in viewCfg) {
|
||||
const Menu = viewCfg.menu;
|
||||
return (
|
||||
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
||||
<Menu />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
||||
<div>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<>
|
||||
@@ -92,7 +76,6 @@ export const TradePanels = ({
|
||||
)}
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<div>{renderMenu()}</div>
|
||||
<div className="h-full">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
@@ -105,12 +88,9 @@ export const TradePanels = ({
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{Object.keys(TradingViews).map((key) => {
|
||||
const isActive = view === key;
|
||||
const className = classNames(
|
||||
'py-2 px-4 min-w-[100px] capitalize text-sm',
|
||||
{
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||
}
|
||||
);
|
||||
const className = classNames('p-4 min-w-[100px] capitalize', {
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
data-testid={key}
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { ComponentProps } from 'react';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import {
|
||||
CandlesChartContainer,
|
||||
CandlesMenu,
|
||||
} from '@vegaprotocol/candles-chart';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
import { Filter } from '@vegaprotocol/orders';
|
||||
import { NO_MARKET } from './constants';
|
||||
import { OrderbookContainer } from '../../components/orderbook-container';
|
||||
@@ -15,7 +12,6 @@ import { AccountsContainer } from '../../components/accounts-container';
|
||||
import { LiquidityContainer } from '../../components/liquidity-container';
|
||||
import type { OrderContainerProps } from '../../components/orders-container';
|
||||
import { OrdersContainer } from '../../components/orders-container';
|
||||
import { StopOrdersContainer } from '../../components/stop-orders-container';
|
||||
|
||||
type MarketDependantView =
|
||||
| typeof CandlesChartContainer
|
||||
@@ -38,7 +34,6 @@ export const TradingViews = {
|
||||
candles: {
|
||||
label: 'Candles',
|
||||
component: requiresMarket(CandlesChartContainer),
|
||||
menu: CandlesMenu,
|
||||
},
|
||||
depth: {
|
||||
label: 'Depth',
|
||||
@@ -79,10 +74,6 @@ export const TradingViews = {
|
||||
label: 'All',
|
||||
component: OrdersContainer,
|
||||
},
|
||||
stopOrders: {
|
||||
label: 'Stop',
|
||||
component: StopOrdersContainer,
|
||||
},
|
||||
collateral: { label: 'Collateral', component: AccountsContainer },
|
||||
fills: { label: 'Fills', component: FillsContainer },
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export const Header = ({ title, children }: TradeMarketHeaderProps) => {
|
||||
);
|
||||
return (
|
||||
<header className={headerClasses}>
|
||||
<div className="hidden lg:flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
|
||||
<div className="flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
|
||||
{title}
|
||||
</div>
|
||||
<div data-testid="header-summary" className="min-w-0">
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const WalletIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="18"
|
||||
viewBox="0 0 26 18"
|
||||
className={classNames('fill-current', className)}
|
||||
className={className}
|
||||
data-testid="wallet-icon"
|
||||
>
|
||||
<path d="M4.77437 17.7499H4.74987C3.6504 17.7368 2.77439 16.8489 2.77439 15.7772V12.8495V12.6343L2.5615 12.6023C1.59672 12.4575 0.849609 11.6116 0.849609 10.6266V7.40064C0.849609 6.39018 1.59509 5.56985 2.56147 5.4249L2.77439 5.39297V5.17767V2.24998C2.77439 1.14102 3.66537 0.25 4.77437 0.25H23.7501C24.8591 0.25 25.7501 1.14098 25.7501 2.24998V15.7499C25.7501 16.8588 24.8591 17.7499 23.7501 17.7499H4.77437ZM4.44917 12.5992H4.19917L4.77441 16.075V16.325H4.77466H23.7502C24.0778 16.325 24.3254 16.0777 24.3254 15.7497V2.24984C24.3254 1.9222 24.0782 1.6746 23.7502 1.6746H4.77441C4.44677 1.6746 4.19917 1.92182 4.19917 2.24984V5.12306V5.37306H4.44917H7.0244C8.51139 5.37306 9.67508 6.56094 9.67508 8.02374V9.94852C9.67508 11.4355 8.4872 12.5992 7.0244 12.5992H4.44917ZM2.84963 6.8253C2.52199 6.8253 2.27439 7.07253 2.27439 7.40054V10.6264C2.27439 10.9541 2.52161 11.2017 2.84962 11.2017L7.02419 11.2019C7.73619 11.2019 8.25009 10.6515 8.25009 9.97598V8.0512C8.25009 7.3392 7.69976 6.8253 7.0242 6.8253H2.84963Z" />
|
||||
|
||||
@@ -11,9 +11,9 @@ export const LayoutWithSidebar = () => {
|
||||
|
||||
const gridClasses = classNames(
|
||||
'h-full relative z-0 grid',
|
||||
'grid-rows-[min-content_1fr_40px]',
|
||||
'lg:grid-rows-[min-content_1fr]',
|
||||
'lg:grid-cols-[1fr_350px_40px]'
|
||||
'grid-rows-[min-content_1fr]',
|
||||
'grid-cols-[1fr_45px]',
|
||||
'lg:grid-cols-[1fr_350px_45px]'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -40,14 +40,7 @@ export const LayoutWithSidebar = () => {
|
||||
>
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800',
|
||||
'border-t lg:border-l lg:border-t-0 border-default',
|
||||
'row-start-3 col-start-1 cols-span-full',
|
||||
'lg:row-start-2 lg:row-span-full lg:col-start-3'
|
||||
)}
|
||||
>
|
||||
<div className="col-start-2 lg:col-start-3 bg-vega-clight-800 dark:bg-vega-cdark-800 border-l border-default">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,8 @@ export const LiquidityHeader = () => {
|
||||
triggeringRatio,
|
||||
});
|
||||
|
||||
console.log(market);
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={
|
||||
|
||||
@@ -4,12 +4,10 @@ import { useParams } from 'react-router-dom';
|
||||
import { MarketSelector } from '../../components/market-selector/market-selector';
|
||||
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
|
||||
import { useMarket } from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const MarketHeader = () => {
|
||||
const { marketId } = useParams();
|
||||
const { data } = useMarket(marketId);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
@@ -17,8 +15,6 @@ export const MarketHeader = () => {
|
||||
<Header
|
||||
title={
|
||||
<Popover
|
||||
open={open}
|
||||
onChange={setOpen}
|
||||
trigger={
|
||||
<HeaderTitle>
|
||||
{data.tradableInstrument.instrument.code}
|
||||
@@ -27,10 +23,7 @@ export const MarketHeader = () => {
|
||||
}
|
||||
alignOffset={-10}
|
||||
>
|
||||
<MarketSelector
|
||||
currentMarketId={marketId}
|
||||
onSelect={() => setOpen(false)}
|
||||
/>
|
||||
<MarketSelector currentMarketId={marketId} />
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -98,7 +98,6 @@ describe('MarketSelectorItem', () => {
|
||||
market={market}
|
||||
currentMarketId={market.id}
|
||||
style={{}}
|
||||
onSelect={jest.fn()}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -17,12 +17,10 @@ export const MarketSelectorItem = ({
|
||||
market,
|
||||
style,
|
||||
currentMarketId,
|
||||
onSelect,
|
||||
}: {
|
||||
market: MarketMaybeWithDataAndCandles;
|
||||
style: CSSProperties;
|
||||
currentMarketId?: string;
|
||||
onSelect: (marketId: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div style={style} role="row">
|
||||
@@ -34,7 +32,6 @@ export const MarketSelectorItem = ({
|
||||
'bg-vega-clight-600 dark:bg-vega-cdark-600':
|
||||
market.id === currentMarketId,
|
||||
})}
|
||||
onClick={() => onSelect(market.id)}
|
||||
>
|
||||
<MarketData market={market} />
|
||||
</Link>
|
||||
@@ -83,7 +80,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-2/5" role="gridcell">
|
||||
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
|
||||
<h3 className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
{market.tradableInstrument.instrument.code}
|
||||
</h3>
|
||||
{mode && (
|
||||
@@ -93,7 +90,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="w-1/5 text-xs lg:text-sm whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
className="w-1/5 text-sm whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
title={instrument.product.settlementAsset.symbol}
|
||||
data-testid="market-selector-price"
|
||||
role="gridcell"
|
||||
@@ -101,7 +98,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
{price} {instrument.product.settlementAsset.symbol}
|
||||
</div>
|
||||
<div
|
||||
className="w-1/5 text-xs lg:text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
className="w-1/5 text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
title={t('24h vol')}
|
||||
data-testid="market-selector-volume"
|
||||
role="gridcell"
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('MarketSelector', () => {
|
||||
it('renders only active markets', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
|
||||
@@ -148,7 +148,7 @@ describe('MarketSelector', () => {
|
||||
it('filters by product type', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('MarketSelector', () => {
|
||||
it('filters by search term', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('MarketSelector', () => {
|
||||
it('filters by asset', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('MarketSelector', () => {
|
||||
it('sorts by gained', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -256,7 +256,7 @@ describe('MarketSelector', () => {
|
||||
it('sorts by lost', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -272,7 +272,7 @@ describe('MarketSelector', () => {
|
||||
it('sorts by new', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const MarketSelector = ({
|
||||
onSelect,
|
||||
}: {
|
||||
currentMarketId?: string;
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect?: (marketId: string) => void;
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<Filter>({
|
||||
searchTerm: '',
|
||||
@@ -48,7 +48,7 @@ export const MarketSelector = ({
|
||||
|
||||
return (
|
||||
<div data-testid="market-selector">
|
||||
<div className="pt-2 px-2 mb-2">
|
||||
<div className="pt-2 px-2 mb-2 w-[320px] lg:w-[584px]">
|
||||
<ProductSelector
|
||||
product={filter.product}
|
||||
onSelect={(product) => {
|
||||
@@ -147,17 +147,16 @@ const MarketList = ({
|
||||
loading: boolean;
|
||||
searchTerm: string;
|
||||
currentMarketId?: string;
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect?: (marketId: string) => void;
|
||||
noItems: string;
|
||||
}) => {
|
||||
const itemSize = 45;
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const rect = listRef.current?.getBoundingClientRect();
|
||||
// allow virtualized list to grow until it runs out of space
|
||||
const computedHeight = rect
|
||||
const height = rect
|
||||
? Math.min(data.length * itemSize, window.innerHeight - rect.y)
|
||||
: 400;
|
||||
const height = Math.max(computedHeight, 45);
|
||||
|
||||
if (error) {
|
||||
return <div>{error.message}</div>;
|
||||
@@ -200,7 +199,7 @@ const MarketList = ({
|
||||
|
||||
interface ListItemData {
|
||||
data: MarketMaybeWithDataAndCandles[];
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect?: (marketId: string) => void;
|
||||
currentMarketId?: string;
|
||||
}
|
||||
|
||||
@@ -217,7 +216,6 @@ const ListItem = ({
|
||||
market={data.data[index]}
|
||||
currentMarketId={data.currentMarketId}
|
||||
style={style}
|
||||
onSelect={data.onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -254,11 +252,7 @@ const List = ({
|
||||
|
||||
if (!data.length) {
|
||||
return (
|
||||
<div
|
||||
style={{ height }}
|
||||
className="flex items-center"
|
||||
data-testid="no-items"
|
||||
>
|
||||
<div style={{ height }} data-testid="no-items">
|
||||
<div className="mx-4 my-2 text-sm">{noItems}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './navbar';
|
||||
export * from './nav-header';
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { MarketSelector } from '../market-selector';
|
||||
import { useMarket } from '@vegaprotocol/markets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* This is only rendered for the mobile navigation
|
||||
*/
|
||||
export const NavHeader = () => {
|
||||
const { marketId } = useParams();
|
||||
const { data } = useMarket(marketId);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!marketId) return null;
|
||||
|
||||
return (
|
||||
<FullScreenPopover
|
||||
open={open}
|
||||
onOpenChange={(x) => {
|
||||
setOpen(x);
|
||||
}}
|
||||
trigger={
|
||||
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-default text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default">
|
||||
{data ? data.tradableInstrument.instrument.code : t('Select market')}
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
<MarketSelector
|
||||
currentMarketId={marketId}
|
||||
onSelect={() => setOpen(false)}
|
||||
/>
|
||||
</FullScreenPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
|
||||
trigger: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export const FullScreenPopover = ({
|
||||
trigger,
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: PopoverProps) => {
|
||||
return (
|
||||
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverPrimitive.Trigger data-testid="popover-trigger">
|
||||
{trigger}
|
||||
</PopoverPrimitive.Trigger>
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-testid="popover-content"
|
||||
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 text-default border border-default"
|
||||
sideOffset={5}
|
||||
>
|
||||
{children}
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
</PopoverPrimitive.Root>
|
||||
);
|
||||
};
|
||||
@@ -1,158 +1,42 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { Navbar } from './navbar';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
|
||||
jest.mock('@vegaprotocol/proposals', () => ({
|
||||
ProtocolUpgradeCountdown: () => null,
|
||||
}));
|
||||
|
||||
describe('Navbar', () => {
|
||||
const pubKey = '000';
|
||||
const pubKeys = [
|
||||
{
|
||||
publicKey: pubKey,
|
||||
name: 'Pub key 0',
|
||||
},
|
||||
{
|
||||
publicKey: '111',
|
||||
name: 'Pub key 1',
|
||||
},
|
||||
];
|
||||
const marketId = 'abc';
|
||||
const navbarContent = 'navbar-menu-content';
|
||||
|
||||
const renderComponent = (
|
||||
initialEntries?: string[],
|
||||
walletContext?: Partial<VegaWalletContextShape>
|
||||
) => {
|
||||
const context = {
|
||||
pubKey,
|
||||
pubKeys,
|
||||
selectPubKey: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
...walletContext,
|
||||
} as VegaWalletContextShape;
|
||||
return render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<VegaWalletContext.Provider value={context}>
|
||||
<Navbar />
|
||||
</VegaWalletContext.Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
useGlobalStore.setState({ marketId });
|
||||
});
|
||||
|
||||
const pubKey = 'pubKey';
|
||||
it('should be properly rendered', () => {
|
||||
renderComponent();
|
||||
|
||||
const expectedLinks = [
|
||||
['/', ''],
|
||||
['/markets/all', 'Markets'],
|
||||
[`/markets/${marketId}`, 'Trading'],
|
||||
['/portfolio', 'Portfolio'],
|
||||
];
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
|
||||
links.forEach((link, i) => {
|
||||
const [href, text] = expectedLinks[i];
|
||||
expect(link).toHaveAttribute('href', href);
|
||||
expect(link).toHaveTextContent(text);
|
||||
});
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Navbar theme="dark" />
|
||||
</VegaWalletContext.Provider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('Markets')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('Trading')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('Portfolio')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Markets page route should not match empty market page', () => {
|
||||
renderComponent(['/markets/all']);
|
||||
expect(screen.getByRole('link', { name: 'Markets' })).toHaveClass('active');
|
||||
expect(screen.getByRole('link', { name: 'Trading' })).not.toHaveClass(
|
||||
'active'
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter initialEntries={['/markets/all']}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Navbar theme="dark" />
|
||||
</VegaWalletContext.Provider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
});
|
||||
|
||||
it('can open menu and navigate on small screens', async () => {
|
||||
renderComponent();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
||||
|
||||
const menuEl = screen.getByTestId(navbarContent);
|
||||
expect(menuEl).toBeInTheDocument();
|
||||
const menu = within(menuEl);
|
||||
|
||||
const expectedLinks = [
|
||||
['/markets/all', 'Markets'],
|
||||
[`/markets/${marketId}`, 'Trading'],
|
||||
['/portfolio', 'Portfolio'],
|
||||
];
|
||||
const links = menu.getAllByRole('link');
|
||||
links.forEach((link, i) => {
|
||||
const [href, text] = expectedLinks[i];
|
||||
expect(link).toHaveAttribute('href', href);
|
||||
expect(link).toHaveTextContent(text);
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Close menu' }));
|
||||
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can close menu by clicking overlay', async () => {
|
||||
renderComponent();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
||||
expect(screen.getByTestId(navbarContent)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByTestId('navbar-menu-overlay'));
|
||||
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can open wallet menu on small screens and change pubkey', async () => {
|
||||
const mockSelectPubKey = jest.fn();
|
||||
renderComponent(undefined, { selectPubKey: mockSelectPubKey });
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
|
||||
|
||||
const menuEl = screen.getByTestId(navbarContent);
|
||||
expect(menuEl).toBeInTheDocument();
|
||||
const menu = within(menuEl);
|
||||
|
||||
expect(menu.getAllByTestId(/key-\d+-mobile/)).toHaveLength(pubKeys.length);
|
||||
|
||||
const activeKey = within(menu.getByTestId('key-000-mobile'));
|
||||
expect(activeKey.getByText(pubKeys[0].name)).toBeInTheDocument();
|
||||
expect(activeKey.getByTestId('icon-tick')).toBeInTheDocument();
|
||||
|
||||
const inactiveKey = within(menu.getByTestId('key-111-mobile'));
|
||||
await userEvent.click(inactiveKey.getByText(pubKeys[1].name));
|
||||
expect(mockSelectPubKey).toHaveBeenCalledWith(pubKeys[1].publicKey);
|
||||
});
|
||||
|
||||
it('can transfer and close menu', async () => {
|
||||
renderComponent();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
|
||||
|
||||
const menuEl = screen.getByTestId(navbarContent);
|
||||
expect(menuEl).toBeInTheDocument();
|
||||
const menu = within(menuEl);
|
||||
|
||||
await userEvent.click(menu.getByText('Transfer'));
|
||||
|
||||
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can disconnect and close menu', async () => {
|
||||
const mockDisconnect = jest.fn();
|
||||
renderComponent(undefined, { disconnect: mockDisconnect });
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
|
||||
|
||||
const menuEl = screen.getByTestId(navbarContent);
|
||||
expect(menuEl).toBeInTheDocument();
|
||||
const menu = within(menuEl);
|
||||
|
||||
await userEvent.click(menu.getByText('Disconnect'));
|
||||
|
||||
expect(mockDisconnect).toHaveBeenCalled();
|
||||
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('Markets')).toHaveClass('active');
|
||||
expect(screen.getByTestId('Trading')).not.toHaveClass('active');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,400 +1,140 @@
|
||||
import type { ButtonHTMLAttributes, LiHTMLAttributes, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEnvironment, DocsLinks, Networks } from '@vegaprotocol/environment';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import {
|
||||
DApp,
|
||||
NetworkSwitcher,
|
||||
TOKEN_GOVERNANCE,
|
||||
useEnvironment,
|
||||
useLinks,
|
||||
DocsLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
|
||||
import { VegaIconNames, VegaIcon, VLogo } from '@vegaprotocol/ui-toolkit';
|
||||
import * as N from '@radix-ui/react-navigation-menu';
|
||||
import * as D from '@radix-ui/react-dialog';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
Navigation,
|
||||
NavigationList,
|
||||
NavigationItem,
|
||||
NavigationLink,
|
||||
ExternalLink,
|
||||
NavigationBreakpoint,
|
||||
NavigationTrigger,
|
||||
NavigationContent,
|
||||
VegaIconNames,
|
||||
VegaIcon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import classNames from 'classnames';
|
||||
import { VegaWalletMenu } from '../vega-wallet';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { WalletIcon } from '../icons/wallet';
|
||||
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
|
||||
|
||||
type MenuState = 'wallet' | 'nav' | null;
|
||||
type Theme = 'system' | 'yellow';
|
||||
import {
|
||||
ProtocolUpgradeCountdown,
|
||||
ProtocolUpgradeCountdownMode,
|
||||
} from '@vegaprotocol/proposals';
|
||||
|
||||
export const Navbar = ({
|
||||
children,
|
||||
theme = 'system',
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
theme?: Theme;
|
||||
theme: ComponentProps<typeof Navigation>['theme'];
|
||||
}) => {
|
||||
// menu state for small screens
|
||||
const [menu, setMenu] = useState<MenuState>(null);
|
||||
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
|
||||
const isConnected = pubKey !== null;
|
||||
|
||||
const navTextClasses = 'text-vega-clight-200 dark:text-vega-cdark-200';
|
||||
const rootClasses = classNames(
|
||||
navTextClasses,
|
||||
'flex gap-3 h-10 pr-1',
|
||||
'border-b border-default',
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800'
|
||||
);
|
||||
return (
|
||||
<N.Root className={rootClasses}>
|
||||
<NavLink
|
||||
to="/"
|
||||
className={classNames('flex items-center px-3', {
|
||||
'bg-vega-yellow text-vega-clight-50': theme === 'yellow',
|
||||
'text-default': theme === 'system',
|
||||
})}
|
||||
>
|
||||
<VLogo className="w-4" />
|
||||
</NavLink>
|
||||
{/* Left section */}
|
||||
<div className="lg:hidden flex items-center">{children}</div>
|
||||
{/* Used to show header in nav on mobile */}
|
||||
<div className="hidden lg:block">
|
||||
<NavbarMenu onClick={() => setMenu(null)} />
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="ml-auto flex justify-end items-center gap-2">
|
||||
<ProtocolUpgradeCountdown />
|
||||
<NavbarMobileButton
|
||||
onClick={() => {
|
||||
if (isConnected) {
|
||||
setMenu((x) => (x === 'wallet' ? null : 'wallet'));
|
||||
} else {
|
||||
openVegaWalletDialog();
|
||||
}
|
||||
}}
|
||||
data-testid="navbar-mobile-wallet"
|
||||
>
|
||||
<span className="sr-only">{t('Wallet')}</span>
|
||||
<WalletIcon className="w-6" />
|
||||
</NavbarMobileButton>
|
||||
<NavbarMobileButton
|
||||
onClick={() => {
|
||||
setMenu((x) => (x === 'nav' ? null : 'nav'));
|
||||
}}
|
||||
data-testid="navbar-mobile-burger"
|
||||
>
|
||||
<span className="sr-only">{t('Menu')}</span>
|
||||
<BurgerIcon />
|
||||
</NavbarMobileButton>
|
||||
<div className="hidden lg:block">
|
||||
<VegaWalletConnectButton />
|
||||
</div>
|
||||
</div>
|
||||
{menu !== null && (
|
||||
<D.Root
|
||||
open={menu !== null}
|
||||
onOpenChange={(open) => setMenu((x) => (open ? x : null))}
|
||||
>
|
||||
<D.Overlay
|
||||
className="lg:hidden fixed inset-0 dark:bg-black/80 bg-black/50 z-20"
|
||||
data-testid="navbar-menu-overlay"
|
||||
/>
|
||||
<D.Content
|
||||
className={classNames(
|
||||
'lg:hidden',
|
||||
'fixed top-0 right-0 z-20 w-3/4 h-screen border-l border-default bg-vega-clight-700 dark:bg-vega-cdark-700',
|
||||
navTextClasses
|
||||
)}
|
||||
data-testid="navbar-menu-content"
|
||||
>
|
||||
<div className="flex justify-end items-center h-10 p-1">
|
||||
<NavbarMobileButton onClick={() => setMenu(null)}>
|
||||
<span className="sr-only">{t('Close menu')}</span>
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={24} />
|
||||
</NavbarMobileButton>
|
||||
</div>
|
||||
{menu === 'nav' && <NavbarMenu onClick={() => setMenu(null)} />}
|
||||
{menu === 'wallet' && <VegaWalletMenu setMenu={setMenu} />}
|
||||
</D.Content>
|
||||
</D.Root>
|
||||
)}
|
||||
</N.Root>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* List of links or dropdown triggers to show in the main section
|
||||
* of the navigation
|
||||
*/
|
||||
const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||
const { GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
const marketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
// If we have a stored marketId make Trade link go to that market
|
||||
// otherwise always go to /markets/all
|
||||
const tradingPath = marketId
|
||||
? Links[Routes.MARKET](marketId)
|
||||
: Links[Routes.MARKET]('');
|
||||
: Links[Routes.MARKET]();
|
||||
|
||||
return (
|
||||
<div className="lg:flex lg:h-full gap-3">
|
||||
<NavbarList>
|
||||
<NavbarItem>
|
||||
<NavbarTrigger data-testid="navbar-network-switcher-trigger">
|
||||
{envNameMapping[VEGA_ENV]}
|
||||
</NavbarTrigger>
|
||||
<NavbarContent data-testid="navbar-content-network-switcher">
|
||||
<ul className="lg:p-4">
|
||||
{[Networks.MAINNET, Networks.TESTNET].map((n) => {
|
||||
const url = VEGA_NETWORKS[n];
|
||||
if (!url) return;
|
||||
return (
|
||||
<NavbarSubItem key={n}>
|
||||
<NavbarLink to={url}>{envNameMapping[n]}</NavbarLink>
|
||||
</NavbarSubItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</NavbarContent>
|
||||
</NavbarItem>
|
||||
</NavbarList>
|
||||
<NavbarListDivider />
|
||||
<NavbarList>
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links[Routes.MARKETS]()} onClick={onClick}>
|
||||
{t('Markets')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLink to={tradingPath} onClick={onClick}>
|
||||
{t('Trading')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links[Routes.PORTFOLIO]()} onClick={onClick}>
|
||||
{t('Portfolio')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
|
||||
<NavbarContent data-testid="navbar-content-resources">
|
||||
<ul className="lg:p-4">
|
||||
{DocsLinks?.NEW_TO_VEGA && (
|
||||
<NavbarSubItem>
|
||||
<NavbarLinkExternal to={DocsLinks?.NEW_TO_VEGA}>
|
||||
{t('Docs')}
|
||||
</NavbarLinkExternal>
|
||||
</NavbarSubItem>
|
||||
)}
|
||||
{GITHUB_FEEDBACK_URL && (
|
||||
<NavbarSubItem>
|
||||
<NavbarLinkExternal to={GITHUB_FEEDBACK_URL}>
|
||||
{t('Give Feedback')}
|
||||
</NavbarLinkExternal>
|
||||
</NavbarSubItem>
|
||||
)}
|
||||
<NavbarSubItem>
|
||||
<NavbarLink to={Links[Routes.DISCLAIMER]()} onClick={onClick}>
|
||||
{t('Disclaimer')}
|
||||
</NavbarLink>
|
||||
</NavbarSubItem>
|
||||
</ul>
|
||||
</NavbarContent>
|
||||
</NavbarItem>
|
||||
</NavbarList>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for radix-ux Trigger for consistent styles
|
||||
*/
|
||||
const NavbarTrigger = ({
|
||||
children,
|
||||
...props
|
||||
}: N.NavigationMenuTriggerProps) => {
|
||||
return (
|
||||
<N.Trigger
|
||||
{...props}
|
||||
onPointerMove={preventHover}
|
||||
onPointerLeave={preventHover}
|
||||
className={classNames(
|
||||
'w-full lg:w-auto lg:h-full',
|
||||
'flex items-center justify-between lg:justify-center gap-2 px-6 py-2 lg:p-0',
|
||||
'text-lg lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
<Navigation
|
||||
appName="console"
|
||||
theme={theme}
|
||||
actions={
|
||||
<>
|
||||
<ProtocolUpgradeCountdown
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
/>
|
||||
<VegaWalletConnectButton />
|
||||
</>
|
||||
}
|
||||
breakpoints={[521, 1122]}
|
||||
>
|
||||
{children}
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
|
||||
</N.Trigger>
|
||||
<NavigationList
|
||||
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
|
||||
hide={[NavigationBreakpoint.Small]}
|
||||
>
|
||||
<NavigationItem className="[.drawer-content_&]:w-full">
|
||||
<NetworkSwitcher className="[.drawer-content_&]:w-full" />
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
<NavigationList
|
||||
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
|
||||
>
|
||||
<NavigationItem>
|
||||
<NavigationLink data-testid="Markets" to={Links[Routes.MARKETS]()}>
|
||||
{t('Markets')}
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink data-testid="Trading" to={tradingPath} end>
|
||||
{t('Trading')}
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink
|
||||
data-testid="Portfolio"
|
||||
to={Links[Routes.PORTFOLIO]()}
|
||||
>
|
||||
{t('Portfolio')}
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavExternalLink href={tokenLink(TOKEN_GOVERNANCE)}>
|
||||
{t('Governance')}
|
||||
</NavExternalLink>
|
||||
</NavigationItem>
|
||||
{DocsLinks?.NEW_TO_VEGA && GITHUB_FEEDBACK_URL && (
|
||||
<NavigationItem>
|
||||
<NavigationTrigger>{t('Resources')}</NavigationTrigger>
|
||||
<NavigationContent>
|
||||
<NavigationList>
|
||||
<NavigationItem>
|
||||
<NavExternalLink href={DocsLinks.NEW_TO_VEGA}>
|
||||
{t('Docs')}
|
||||
</NavExternalLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavExternalLink href={GITHUB_FEEDBACK_URL}>
|
||||
{t('Give Feedback')}
|
||||
</NavExternalLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink
|
||||
data-testid="Disclaimer"
|
||||
to={Links[Routes.DISCLAIMER]()}
|
||||
>
|
||||
{t('Disclaimer')}
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
</NavigationContent>
|
||||
</NavigationItem>
|
||||
)}
|
||||
</NavigationList>
|
||||
</Navigation>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for react-router-dom NavLink for consistent styles
|
||||
*/
|
||||
const NavbarLink = ({
|
||||
const NavExternalLink = ({
|
||||
children,
|
||||
to,
|
||||
onClick,
|
||||
href,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
to: string;
|
||||
onClick?: () => void;
|
||||
href: string;
|
||||
}) => {
|
||||
return (
|
||||
<N.Link asChild={true}>
|
||||
<NavLink
|
||||
to={to}
|
||||
className={classNames(
|
||||
'block lg:flex lg:h-full flex-col justify-center',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{({ isActive }) => {
|
||||
const borderClasses = {
|
||||
'border-b-2': true,
|
||||
'border-transparent': !isActive,
|
||||
'border-vega-yellow lg:group-[.navbar-content]:border-transparent':
|
||||
isActive,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={classNames('lg:border-0', borderClasses, {
|
||||
'text-vega-clight-50 dark:text-vega-cdark-50': isActive,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'hidden lg:block absolute left-0 bottom-0 w-full h-0',
|
||||
borderClasses
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</NavLink>
|
||||
</N.Link>
|
||||
);
|
||||
};
|
||||
|
||||
const NavbarItem = (props: N.NavigationMenuItemProps) => {
|
||||
return <N.Item {...props} className="relative" />;
|
||||
};
|
||||
|
||||
const NavbarSubItem = (props: LiHTMLAttributes<HTMLElement>) => {
|
||||
return <li {...props} className="lg:mb-4 lg:last:mb-0" />;
|
||||
};
|
||||
|
||||
const NavbarList = (props: N.NavigationMenuListProps) => {
|
||||
return <N.List {...props} className="lg:flex lg:h-full gap-6" />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Content that gets rendered when a sub section of the navbar is shown
|
||||
*/
|
||||
const NavbarContent = (props: N.NavigationMenuContentProps) => {
|
||||
return (
|
||||
<N.Content
|
||||
{...props}
|
||||
className={classNames(
|
||||
'group navbar-content',
|
||||
'lg:absolute lg:mt-2 pl-2 lg:pl-0 z-20 lg:min-w-[290px]',
|
||||
'lg:bg-vega-clight-700 lg:dark:bg-vega-cdark-700',
|
||||
'lg:border border-vega-clight-500 dark:border-vega-cdark-500 lg:rounded'
|
||||
)}
|
||||
onPointerEnter={preventHover}
|
||||
onPointerLeave={preventHover}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* NavbarLink with OPEN_EXTERNAL icon
|
||||
*/
|
||||
const NavbarLinkExternal = ({
|
||||
children,
|
||||
to,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
to: string;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<N.Link asChild={true}>
|
||||
<NavLink
|
||||
to={to}
|
||||
className={classNames(
|
||||
'flex lg:inline-flex gap-2 justify-between items-center relative',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
onClick={onClick}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink href={href}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{children}</span>
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</NavLink>
|
||||
</N.Link>
|
||||
</span>
|
||||
</ExternalLink>
|
||||
);
|
||||
};
|
||||
|
||||
const BurgerIcon = () => (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 16 16"
|
||||
className="w-full stroke-current"
|
||||
>
|
||||
<line x1={0.5} x2={15.5} y1={3.5} y2={3.5} />
|
||||
<line x1={0.5} x2={15.5} y1={11.5} y2={11.5} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const NavbarListDivider = () => {
|
||||
return (
|
||||
<div className="py-2 px-6 lg:px-0" role="separator">
|
||||
<div className="h-px lg:h-full w-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Button component to avoid repeating styles for buttons shown on small screens
|
||||
*/
|
||||
const NavbarMobileButton = (props: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={classNames(
|
||||
'w-8 h-8 lg:hidden flex items-center p-1 rounded ',
|
||||
'hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500',
|
||||
'hover:text-vega-clight-50 dark:hover:text-vega-cdark-50'
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const envNameMapping: Record<Networks, string> = {
|
||||
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET'),
|
||||
[Networks.CUSTOM]: t('Custom'),
|
||||
[Networks.DEVNET]: t('Devnet'),
|
||||
[Networks.STAGNET1]: t('Stagnet'),
|
||||
[Networks.TESTNET]: t('Fairground testnet'),
|
||||
[Networks.MAINNET_MIRROR]: t('Mirror'),
|
||||
[Networks.MAINNET]: t('Mainnet'),
|
||||
};
|
||||
|
||||
// https://github.com/radix-ui/primitives/issues/1630
|
||||
// eslint-disable-next-line
|
||||
const preventHover = (e: any) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
|
||||
|
||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||
const useOrderStoreRef = useCreateOrderStore();
|
||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
||||
const updateStoredFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
);
|
||||
const setView = useSidebar((store) => store.setView);
|
||||
return (
|
||||
<OrderbookManager
|
||||
@@ -16,11 +12,9 @@ export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||
onClick={({ price, size }) => {
|
||||
if (price) {
|
||||
updateOrder(marketId, { price });
|
||||
updateStoredFormValues(marketId, { price });
|
||||
}
|
||||
if (size) {
|
||||
updateOrder(marketId, { size });
|
||||
updateStoredFormValues(marketId, { size });
|
||||
}
|
||||
setView({ type: ViewType.Order });
|
||||
}}
|
||||
|
||||
@@ -51,10 +51,9 @@ type SidebarView =
|
||||
};
|
||||
|
||||
export const Sidebar = () => {
|
||||
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
|
||||
return (
|
||||
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
|
||||
<nav className={navClasses}>
|
||||
<div className="flex flex-col gap-2 h-full py-1" data-testid="sidebar">
|
||||
<nav className="flex flex-col items-center gap-4 p-1">
|
||||
{/* sidebar options that always show */}
|
||||
<SidebarButton
|
||||
view={ViewType.Deposit}
|
||||
@@ -103,7 +102,7 @@ export const Sidebar = () => {
|
||||
/>
|
||||
</Routes>
|
||||
</nav>
|
||||
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
|
||||
<nav className="mt-auto flex flex-col items-center gap-4 p-1">
|
||||
<SidebarButton
|
||||
view={ViewType.Settings}
|
||||
icon={VegaIconNames.COG}
|
||||
@@ -162,7 +161,7 @@ const SidebarButton = ({
|
||||
const SidebarDivider = () => {
|
||||
return (
|
||||
<div
|
||||
className="bg-vega-clight-600 dark:bg-vega-cdark-600 w-px h-4 lg:w-4 lg:h-px"
|
||||
className="bg-vega-clight-600 dark:bg-vega-cdark-600 w-4 h-px"
|
||||
role="separator"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './stop-orders-container';
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { StopOrdersManager } from '@vegaprotocol/orders';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
|
||||
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
|
||||
|
||||
export const StopOrdersContainer = () => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
|
||||
const gridStore = useStopOrdersStore((store) => store.gridStore);
|
||||
const updateGridStore = useStopOrdersStore((store) => store.updateGridStore);
|
||||
|
||||
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
|
||||
updateGridStore(colState);
|
||||
});
|
||||
|
||||
if (!pubKey) {
|
||||
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StopOrdersManager
|
||||
partyId={pubKey}
|
||||
onMarketClick={onMarketClick}
|
||||
isReadOnly={isReadOnly}
|
||||
gridProps={gridStoreCallbacks}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const useStopOrdersStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_fills_store',
|
||||
})
|
||||
);
|
||||
+1
-1
@@ -28,7 +28,7 @@ describe('VegaWalletConnectButton', () => {
|
||||
render(generateJsx({ pubKey: null } as VegaWalletContextShape));
|
||||
|
||||
const button = screen.getByTestId('connect-vega-wallet');
|
||||
expect(button).toHaveTextContent('Connect');
|
||||
expect(button).toHaveTextContent('Connect Vega wallet');
|
||||
fireEvent.click(button);
|
||||
expect(mockUpdateDialogOpen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
+252
-96
@@ -1,26 +1,148 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import classNames from 'classnames';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
Drawer,
|
||||
DropdownMenuSeparator,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
TradingButton as Button,
|
||||
Intent,
|
||||
TradingDropdown,
|
||||
TradingDropdownTrigger,
|
||||
TradingDropdownContent,
|
||||
TradingDropdownRadioGroup,
|
||||
TradingDropdownSeparator,
|
||||
TradingDropdownItem,
|
||||
TradingDropdownRadioItem,
|
||||
TradingDropdownItemIndicator,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { PubKey } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { WalletIcon } from '../icons/wallet';
|
||||
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
|
||||
import { ViewType, useSidebar } from '../sidebar';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MobileWalletButton = ({
|
||||
isConnected,
|
||||
activeKey,
|
||||
}: {
|
||||
isConnected?: boolean;
|
||||
activeKey?: PubKey;
|
||||
}) => {
|
||||
const { pubKeys, selectPubKey, disconnect, fetchPubKeys } = useVegaWallet();
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
const setView = useSidebar((store) => store.setView);
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const isYellow = VEGA_ENV === Networks.TESTNET;
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const mobileDisconnect = useCallback(() => {
|
||||
setDrawerOpen(false);
|
||||
disconnect();
|
||||
}, [disconnect]);
|
||||
const openDrawer = useCallback(() => {
|
||||
if (!isConnected) {
|
||||
openVegaWalletDialog();
|
||||
setDrawerOpen(false);
|
||||
} else {
|
||||
if (fetchPubKeys) {
|
||||
fetchPubKeys();
|
||||
}
|
||||
setDrawerOpen(!drawerOpen);
|
||||
}
|
||||
}, [drawerOpen, fetchPubKeys, isConnected, openVegaWalletDialog]);
|
||||
|
||||
const iconClass = drawerOpen
|
||||
? 'hidden'
|
||||
: isYellow
|
||||
? 'fill-black'
|
||||
: 'fill-white';
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
||||
|
||||
const walletButton = (
|
||||
<button
|
||||
className="my-2 transition-all flex flex-col justify-around gap-3 p-2 relative h-[34px]"
|
||||
onClick={openDrawer}
|
||||
data-testid="connect-vega-wallet-mobile"
|
||||
>
|
||||
<WalletIcon className={iconClass} />
|
||||
</button>
|
||||
);
|
||||
const onSelectItem = useCallback(
|
||||
(pubkey: string) => {
|
||||
setDrawerOpen(false);
|
||||
selectPubKey(pubkey);
|
||||
},
|
||||
[selectPubKey]
|
||||
);
|
||||
return (
|
||||
<div className="lg:hidden overflow-hidden flex" ref={setContainer}>
|
||||
<Drawer
|
||||
dataTestId="wallets-drawer"
|
||||
open={drawerOpen}
|
||||
onChange={setDrawerOpen}
|
||||
container={container}
|
||||
trigger={walletButton}
|
||||
>
|
||||
<div className="border-l border-default p-2 gap-4 flex flex-col w-full h-full bg-white dark:bg-black dark:text-white justify-between">
|
||||
<div className="flex h-5 justify-end">
|
||||
<button
|
||||
className="transition-all flex flex-col justify-around gap-3 p-2 relative h-[34px]"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
data-testid="connect-vega-wallet-mobile-close"
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'w-[26px] h-[2px] bg-black dark:bg-white transition-all translate-y-[7.5px] rotate-45',
|
||||
{
|
||||
hidden: !drawerOpen,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'w-[26px] h-[2px] bg-black dark:bg-white transition-all -translate-y-[7.5px] -rotate-45',
|
||||
{
|
||||
hidden: !drawerOpen,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
</button>
|
||||
</div>
|
||||
<div className="grow my-4" role="list">
|
||||
{(pubKeys || []).map((pk) => (
|
||||
<KeypairListItem
|
||||
key={pk.publicKey}
|
||||
pk={pk}
|
||||
isActive={activeKey?.publicKey === pk.publicKey}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 m-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDrawerOpen(false);
|
||||
setView({ type: ViewType.Transfer });
|
||||
}}
|
||||
fill
|
||||
>
|
||||
{t('Transfer')}
|
||||
</Button>
|
||||
<Button onClick={mobileDisconnect} fill>
|
||||
{t('Disconnect')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const VegaWalletConnectButton = () => {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
@@ -44,101 +166,96 @@ export const VegaWalletConnectButton = () => {
|
||||
|
||||
if (isConnected && pubKeys) {
|
||||
return (
|
||||
<TradingDropdown
|
||||
open={dropdownOpen}
|
||||
trigger={
|
||||
<TradingDropdownTrigger
|
||||
data-testid="manage-vega-wallet"
|
||||
onClick={() => {
|
||||
if (fetchPubKeys) {
|
||||
fetchPubKeys();
|
||||
}
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />}
|
||||
>
|
||||
{activeKey && <span className="uppercase">{activeKey.name}</span>}
|
||||
{' | '}
|
||||
{truncateByChars(pubKey)}
|
||||
</Button>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent
|
||||
onInteractOutside={() => setDropdownOpen(false)}
|
||||
sideOffset={12}
|
||||
side="bottom"
|
||||
align="end"
|
||||
onEscapeKeyDown={() => setDropdownOpen(false)}
|
||||
>
|
||||
<div className="min-w-[340px]" data-testid="keypair-list">
|
||||
<TradingDropdownRadioGroup
|
||||
value={pubKey}
|
||||
onValueChange={(value) => {
|
||||
selectPubKey(value);
|
||||
}}
|
||||
>
|
||||
{pubKeys.map((pk) => (
|
||||
<KeypairItem
|
||||
key={pk.publicKey}
|
||||
pk={pk}
|
||||
active={pk.publicKey === pubKey}
|
||||
/>
|
||||
))}
|
||||
</TradingDropdownRadioGroup>
|
||||
<TradingDropdownSeparator />
|
||||
{!isReadOnly && (
|
||||
<TradingDropdownItem
|
||||
data-testid="wallet-transfer"
|
||||
<>
|
||||
<div className="hidden lg:block">
|
||||
<DropdownMenu
|
||||
open={dropdownOpen}
|
||||
trigger={
|
||||
<DropdownMenuTrigger
|
||||
data-testid="manage-vega-wallet"
|
||||
onClick={() => {
|
||||
setView({ type: ViewType.Transfer });
|
||||
setDropdownOpen(false);
|
||||
if (fetchPubKeys) {
|
||||
fetchPubKeys();
|
||||
}
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
}}
|
||||
>
|
||||
{t('Transfer')}
|
||||
</TradingDropdownItem>
|
||||
)}
|
||||
<TradingDropdownItem data-testid="disconnect" onClick={disconnect}>
|
||||
{t('Disconnect')}
|
||||
</TradingDropdownItem>
|
||||
</div>
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
{activeKey && (
|
||||
<span className="uppercase">{activeKey.name}</span>
|
||||
)}
|
||||
{': '}
|
||||
{truncateByChars(pubKey)}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent
|
||||
onInteractOutside={() => setDropdownOpen(false)}
|
||||
sideOffset={17}
|
||||
side="bottom"
|
||||
align="end"
|
||||
onEscapeKeyDown={() => setDropdownOpen(false)}
|
||||
>
|
||||
<div className="min-w-[340px]" data-testid="keypair-list">
|
||||
<DropdownMenuRadioGroup
|
||||
value={pubKey}
|
||||
onValueChange={(value) => {
|
||||
selectPubKey(value);
|
||||
}}
|
||||
>
|
||||
{pubKeys.map((pk) => (
|
||||
<KeypairItem key={pk.publicKey} pk={pk} />
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
<DropdownMenuSeparator />
|
||||
{!isReadOnly && (
|
||||
<DropdownMenuItem
|
||||
data-testid="wallet-transfer"
|
||||
onClick={() => {
|
||||
setView({ type: ViewType.Transfer });
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('Transfer')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem data-testid="disconnect" onClick={disconnect}>
|
||||
{t('Disconnect')}
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<MobileWalletButton isConnected activeKey={activeKey} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-testid="connect-vega-wallet"
|
||||
onClick={openVegaWalletDialog}
|
||||
size="small"
|
||||
intent={Intent.None}
|
||||
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
|
||||
>
|
||||
<span className="whitespace-nowrap uppercase">{t('Connect')}</span>
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
data-testid="connect-vega-wallet"
|
||||
onClick={openVegaWalletDialog}
|
||||
size="sm"
|
||||
className="hidden lg:block"
|
||||
>
|
||||
<span className="whitespace-nowrap">{t('Connect Vega wallet')}</span>
|
||||
</Button>
|
||||
<MobileWalletButton />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const KeypairItem = ({ pk, active }: { pk: PubKey; active: boolean }) => {
|
||||
const KeypairItem = ({ pk }: { pk: PubKey }) => {
|
||||
const [copied, setCopied] = useCopyTimeout();
|
||||
|
||||
return (
|
||||
<TradingDropdownRadioItem value={pk.publicKey}>
|
||||
<div
|
||||
className={classNames('flex-1 mr-2', {
|
||||
'text-default': active,
|
||||
'text-muted': !active,
|
||||
})}
|
||||
data-testid={`key-${pk.publicKey}`}
|
||||
>
|
||||
<span className={classNames('mr-2 uppercase')}>
|
||||
{pk.name}
|
||||
{' | '}
|
||||
{truncateByChars(pk.publicKey)}
|
||||
<DropdownMenuRadioItem value={pk.publicKey}>
|
||||
<div className="flex-1 mr-2" data-testid={`key-${pk.publicKey}`}>
|
||||
<span className="mr-2">
|
||||
<span>
|
||||
<span className="uppercase">{pk.name}</span>:{' '}
|
||||
{truncateByChars(pk.publicKey)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
|
||||
@@ -153,7 +270,46 @@ const KeypairItem = ({ pk, active }: { pk: PubKey; active: boolean }) => {
|
||||
{copied && <span className="text-xs">{t('Copied')}</span>}
|
||||
</span>
|
||||
</div>
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownRadioItem>
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
};
|
||||
|
||||
const KeypairListItem = ({
|
||||
pk,
|
||||
isActive,
|
||||
onSelectItem,
|
||||
}: {
|
||||
pk: PubKey;
|
||||
isActive: boolean;
|
||||
onSelectItem: (pk: string) => void;
|
||||
}) => {
|
||||
const [copied, setCopied] = useCopyTimeout();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full ml-4 mr-2 mb-4"
|
||||
data-testid={`key-${pk.publicKey}-mobile`}
|
||||
>
|
||||
<span className="flex gap-2 items-center mr-2">
|
||||
<button onClick={() => onSelectItem(pk.publicKey)}>
|
||||
<span className="uppercase">{pk.name}</span>
|
||||
</button>
|
||||
{isActive && <VegaIcon name={VegaIconNames.TICK} />}
|
||||
</span>
|
||||
<span className="flex gap-2 items-center">
|
||||
{truncateByChars(pk.publicKey)}{' '}
|
||||
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
|
||||
<button
|
||||
data-testid="copy-vega-public-key"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<VegaIcon name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
{copied && <span className="text-xs">{t('Copied')}</span>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { VegaWalletMenu } from './vega-wallet-menu';
|
||||
@@ -1,106 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
TradingButton as Button,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { useVegaWallet, type PubKey } from '@vegaprotocol/wallet';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { ViewType, useSidebar } from '../sidebar';
|
||||
|
||||
export const VegaWalletMenu = ({
|
||||
setMenu,
|
||||
}: {
|
||||
setMenu: (open: 'nav' | 'wallet' | null) => void;
|
||||
}) => {
|
||||
const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet();
|
||||
const setView = useSidebar((store) => store.setView);
|
||||
|
||||
const activeKey = useMemo(() => {
|
||||
return pubKeys?.find((pk) => pk.publicKey === pubKey);
|
||||
}, [pubKey, pubKeys]);
|
||||
|
||||
const onSelectItem = useCallback(
|
||||
(pubkey: string) => {
|
||||
selectPubKey(pubkey);
|
||||
},
|
||||
[selectPubKey]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grow my-4" role="list">
|
||||
{(pubKeys || []).map((pk) => (
|
||||
<KeypairListItem
|
||||
key={pk.publicKey}
|
||||
pk={pk}
|
||||
isActive={activeKey?.publicKey === pk.publicKey}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 m-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setView({ type: ViewType.Transfer });
|
||||
setMenu(null);
|
||||
}}
|
||||
>
|
||||
{t('Transfer')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await disconnect();
|
||||
setMenu(null);
|
||||
}}
|
||||
>
|
||||
{t('Disconnect')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const KeypairListItem = ({
|
||||
pk,
|
||||
isActive,
|
||||
onSelectItem,
|
||||
}: {
|
||||
pk: PubKey;
|
||||
isActive: boolean;
|
||||
onSelectItem: (pk: string) => void;
|
||||
}) => {
|
||||
const [copied, setCopied] = useCopyTimeout();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full ml-4 mr-2 mb-4"
|
||||
data-testid={`key-${pk.publicKey}-mobile`}
|
||||
>
|
||||
<span className="flex gap-2 items-center mr-2">
|
||||
<button type="button" onClick={() => onSelectItem(pk.publicKey)}>
|
||||
<span className="uppercase">{pk.name}</span>
|
||||
</button>
|
||||
{isActive && <VegaIcon name={VegaIconNames.TICK} />}
|
||||
</span>
|
||||
<span className="flex gap-2 items-center">
|
||||
{truncateByChars(pk.publicKey)}{' '}
|
||||
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="copy-vega-public-key"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<VegaIcon name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
{copied && <span className="text-xs">{t('Copied')}</span>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from '@vegaprotocol/web3';
|
||||
import {
|
||||
envTriggerMapping,
|
||||
Networks,
|
||||
NodeSwitcherDialog,
|
||||
useEnvironment,
|
||||
useInitializeEnv,
|
||||
@@ -26,13 +25,7 @@ import './styles.css';
|
||||
import { usePageTitleStore } from '../stores';
|
||||
import DialogsContainer from './dialogs-container';
|
||||
import ToastsManager from './toasts-manager';
|
||||
import {
|
||||
HashRouter,
|
||||
useLocation,
|
||||
Route,
|
||||
Routes,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -46,8 +39,6 @@ import {
|
||||
ProtocolUpgradeProposalNotification,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { NavHeader } from '../components/navbar/nav-header';
|
||||
import { Routes as AppRoutes } from './client-router';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@@ -83,7 +74,6 @@ const InitializeHandlers = () => {
|
||||
|
||||
function AppBody({ Component }: AppProps) {
|
||||
const location = useLocation();
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const gridClasses = classNames(
|
||||
'h-full relative z-0 grid',
|
||||
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
|
||||
@@ -97,16 +87,7 @@ function AppBody({ Component }: AppProps) {
|
||||
<Title />
|
||||
<div className={gridClasses}>
|
||||
<AnnouncementBanner />
|
||||
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'}>
|
||||
<Routes>
|
||||
<Route
|
||||
path={AppRoutes.MARKETS}
|
||||
// render nothing for markets/all, otherwise markets/:marketId will match with markets/all
|
||||
element={null}
|
||||
/>
|
||||
<Route path={AppRoutes.MARKET} element={<NavHeader />} />
|
||||
</Routes>
|
||||
</Navbar>
|
||||
<Navbar theme="system" />
|
||||
<div data-testid="banners">
|
||||
<ProtocolUpgradeProposalNotification
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const useAccountBalance = (assetId?: string) => {
|
||||
},
|
||||
[assetId]
|
||||
);
|
||||
const { loading, error } = useDataProvider({
|
||||
useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables,
|
||||
skip: !pubKey || !assetId,
|
||||
@@ -34,9 +34,7 @@ export const useAccountBalance = (assetId?: string) => {
|
||||
() => ({
|
||||
accountBalance: pubKey ? accountBalance : '',
|
||||
accountDecimals: pubKey ? accountDecimals : null,
|
||||
loading,
|
||||
error,
|
||||
}),
|
||||
[accountBalance, accountDecimals, pubKey, loading, error]
|
||||
[accountBalance, accountDecimals, pubKey]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const useMarketAccountBalance = (marketId: string) => {
|
||||
},
|
||||
[marketId]
|
||||
);
|
||||
const { loading, error } = useDataProvider({
|
||||
useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey || !marketId,
|
||||
@@ -33,9 +33,7 @@ export const useMarketAccountBalance = (marketId: string) => {
|
||||
() => ({
|
||||
accountBalance: pubKey ? accountBalance : '',
|
||||
accountDecimals: pubKey ? accountDecimals : null,
|
||||
loading,
|
||||
error,
|
||||
}),
|
||||
[accountBalance, accountDecimals, pubKey, loading, error]
|
||||
[accountBalance, accountDecimals, pubKey]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "@vegaprotocol/announcements",
|
||||
"version": "0.0.2"
|
||||
"version": "0.0.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { CandlesChartContainer } from './candles-chart';
|
||||
import { render, screen, waitFor, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { CandlesEventsDocument } from './__generated__/Candles';
|
||||
import type { CandlesEventsSubscription } from './__generated__/Candles';
|
||||
|
||||
const candles: CandlesEventsSubscription = {
|
||||
candles: {
|
||||
lastUpdateInPeriod: 0,
|
||||
periodStart: 0,
|
||||
open: '0',
|
||||
high: '0',
|
||||
low: '0',
|
||||
close: '0',
|
||||
volume: '0',
|
||||
},
|
||||
};
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: CandlesEventsDocument,
|
||||
variables: { marketId: 'market-id', interval: 'INTERVAL_I15M' },
|
||||
},
|
||||
result: { data: candles },
|
||||
},
|
||||
];
|
||||
|
||||
describe('TradingChart', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={{} as never}>
|
||||
<CandlesChartContainer marketId={'market-id'} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('volume study should be preselected', async () => {
|
||||
act(() => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider value={{} as never}>
|
||||
<CandlesChartContainer marketId={'market-id'} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('Studies', {
|
||||
selector: '[type="button"]',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
act(() => {
|
||||
userEvent.click(
|
||||
screen.getByText('Studies', {
|
||||
selector: '[type="button"]',
|
||||
})
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,92 @@
|
||||
import 'pennant/dist/style.css';
|
||||
import { CandlestickChart } from 'pennant';
|
||||
import {
|
||||
CandlestickChart,
|
||||
ChartType,
|
||||
Interval,
|
||||
Overlay,
|
||||
Study,
|
||||
chartTypeLabels,
|
||||
intervalLabels,
|
||||
overlayLabels,
|
||||
studyLabels,
|
||||
} from 'pennant';
|
||||
import { VegaDataSource } from './data-source';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useThemeSwitcher,
|
||||
getValidItem,
|
||||
getValidSubset,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { IconName } from '@blueprintjs/icons';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCandlesChartSettings } from './use-candles-chart-settings';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
interface StoredSettings {
|
||||
interval?: Interval;
|
||||
type?: ChartType;
|
||||
overlays?: Overlay[];
|
||||
studies?: Study[];
|
||||
}
|
||||
|
||||
export const useCandlesChartSettings = create<
|
||||
StoredSettings & {
|
||||
merge: (settings: StoredSettings) => void;
|
||||
setType: (type: ChartType) => void;
|
||||
setInterval: (interval: Interval) => void;
|
||||
setOverlays: (overlays: Overlay[]) => void;
|
||||
setStudies: (studies: Study[]) => void;
|
||||
}
|
||||
>()(
|
||||
persist(
|
||||
immer((set) => ({
|
||||
merge: (settings: StoredSettings) =>
|
||||
set((state) => {
|
||||
Object.assign(state, settings);
|
||||
}),
|
||||
setType: (type: ChartType) =>
|
||||
set((state) => {
|
||||
state.type = type;
|
||||
}),
|
||||
setInterval: (interval: Interval) =>
|
||||
set((state) => {
|
||||
state.interval = interval;
|
||||
}),
|
||||
setOverlays: (overlays: Overlay[]) =>
|
||||
set((state) => {
|
||||
state.overlays = overlays;
|
||||
}),
|
||||
setStudies: (studies: Study[]) =>
|
||||
set((state) => {
|
||||
state.studies = studies;
|
||||
}),
|
||||
})),
|
||||
{
|
||||
name: 'console-candles',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const chartTypeIcon = new Map<ChartType, IconName>([
|
||||
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
|
||||
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
|
||||
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
|
||||
[ChartType.OHLC, IconNames.WATERFALL_CHART],
|
||||
]);
|
||||
|
||||
export type CandlesChartContainerProps = {
|
||||
marketId: string;
|
||||
@@ -19,32 +99,161 @@ export const CandlesChartContainer = ({
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { theme } = useThemeSwitcher();
|
||||
|
||||
const { interval, chartType, overlays, studies, merge } =
|
||||
useCandlesChartSettings();
|
||||
const settings = useCandlesChartSettings();
|
||||
|
||||
const interval: Interval = getValidItem(
|
||||
settings.interval,
|
||||
Object.values(Interval),
|
||||
Interval.I15M
|
||||
);
|
||||
|
||||
const chartType: ChartType = getValidItem(
|
||||
settings.type,
|
||||
Object.values(ChartType),
|
||||
ChartType.CANDLE
|
||||
);
|
||||
|
||||
const overlays: Overlay[] = getValidSubset(
|
||||
settings.overlays,
|
||||
Object.values(Overlay),
|
||||
[]
|
||||
);
|
||||
|
||||
const studies: Study[] = getValidSubset(
|
||||
settings.studies,
|
||||
Object.values(Study),
|
||||
[Study.VOLUME]
|
||||
);
|
||||
|
||||
const dataSource = useMemo(() => {
|
||||
return new VegaDataSource(client, marketId, pubKey);
|
||||
}, [client, marketId, pubKey]);
|
||||
|
||||
return (
|
||||
<CandlestickChart
|
||||
dataSource={dataSource}
|
||||
options={{
|
||||
chartType,
|
||||
overlays,
|
||||
studies,
|
||||
notEnoughDataText: (
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
),
|
||||
}}
|
||||
interval={interval}
|
||||
theme={theme}
|
||||
onOptionsChanged={(options) => {
|
||||
merge({
|
||||
overlays: options.overlays,
|
||||
studies: options.studies,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-3 lg:px-4 py-2 flex flex-row flex-wrap gap-2 bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{t(`Interval: ${intervalLabels[interval]}`)}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={interval}
|
||||
onValueChange={(value) => {
|
||||
settings.setInterval(value as Interval);
|
||||
}}
|
||||
>
|
||||
{Object.values(Interval).map((timeInterval) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={timeInterval}
|
||||
inset
|
||||
value={timeInterval}
|
||||
>
|
||||
{intervalLabels[timeInterval]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
<Icon name={chartTypeIcon.get(chartType) as IconName} />
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={chartType}
|
||||
onValueChange={(value) => {
|
||||
settings.setType(value as ChartType);
|
||||
}}
|
||||
>
|
||||
{Object.values(ChartType).map((type) => (
|
||||
<DropdownMenuRadioItem key={type} inset value={type}>
|
||||
{chartTypeLabels[type]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={<DropdownMenuTrigger>{t('Overlays')}</DropdownMenuTrigger>}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{Object.values(Overlay).map((overlay) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={overlay}
|
||||
checked={overlays.includes(overlay)}
|
||||
onCheckedChange={() => {
|
||||
const newOverlays = [...overlays];
|
||||
const index = overlays.findIndex((item) => item === overlay);
|
||||
|
||||
index !== -1
|
||||
? newOverlays.splice(index, 1)
|
||||
: newOverlays.push(overlay);
|
||||
|
||||
settings.setOverlays(newOverlays);
|
||||
}}
|
||||
>
|
||||
{overlayLabels[overlay]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={<DropdownMenuTrigger>{t('Studies')}</DropdownMenuTrigger>}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{Object.values(Study).map((study) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={study}
|
||||
checked={studies.includes(study)}
|
||||
onCheckedChange={() => {
|
||||
const newStudies = [...studies];
|
||||
const index = studies.findIndex((item) => item === study);
|
||||
|
||||
index !== -1
|
||||
? newStudies.splice(index, 1)
|
||||
: newStudies.push(study);
|
||||
|
||||
settings.setStudies(newStudies);
|
||||
}}
|
||||
>
|
||||
{studyLabels[study]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CandlestickChart
|
||||
dataSource={dataSource}
|
||||
options={{
|
||||
chartType: chartType,
|
||||
overlays: overlays,
|
||||
studies: studies,
|
||||
notEnoughDataText: (
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
),
|
||||
}}
|
||||
interval={interval}
|
||||
theme={theme}
|
||||
onOptionsChanged={(options) => {
|
||||
settings.merge({
|
||||
overlays: options.overlays,
|
||||
studies: options.studies,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CandlesMenu } from './candles-menu';
|
||||
|
||||
describe('CandlesMenu', () => {
|
||||
it('should render with volume study showing by default', async () => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByText('Studies', {
|
||||
selector: '[type="button"]',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
|
||||
});
|
||||
});
|
||||
@@ -1,160 +0,0 @@
|
||||
import 'pennant/dist/style.css';
|
||||
import {
|
||||
ChartType,
|
||||
Interval,
|
||||
Overlay,
|
||||
Study,
|
||||
chartTypeLabels,
|
||||
intervalLabels,
|
||||
overlayLabels,
|
||||
studyLabels,
|
||||
} from 'pennant';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { IconName } from '@blueprintjs/icons';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCandlesChartSettings } from './use-candles-chart-settings';
|
||||
|
||||
const chartTypeIcon = new Map<ChartType, IconName>([
|
||||
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
|
||||
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
|
||||
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
|
||||
[ChartType.OHLC, IconNames.WATERFALL_CHART],
|
||||
]);
|
||||
|
||||
export const CandlesMenu = () => {
|
||||
const {
|
||||
interval,
|
||||
chartType,
|
||||
studies,
|
||||
overlays,
|
||||
setInterval,
|
||||
setType,
|
||||
setStudies,
|
||||
setOverlays,
|
||||
} = useCandlesChartSettings();
|
||||
const triggerClasses = 'text-xs';
|
||||
const contentAlign = 'end';
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
{t(`Interval: ${intervalLabels[interval]}`)}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
<DropdownMenuRadioGroup
|
||||
value={interval}
|
||||
onValueChange={(value) => {
|
||||
setInterval(value as Interval);
|
||||
}}
|
||||
>
|
||||
{Object.values(Interval).map((timeInterval) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={timeInterval}
|
||||
inset
|
||||
value={timeInterval}
|
||||
>
|
||||
{intervalLabels[timeInterval]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
<Icon name={chartTypeIcon.get(chartType) as IconName} />
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
<DropdownMenuRadioGroup
|
||||
value={chartType}
|
||||
onValueChange={(value) => {
|
||||
setType(value as ChartType);
|
||||
}}
|
||||
>
|
||||
{Object.values(ChartType).map((type) => (
|
||||
<DropdownMenuRadioItem key={type} inset value={type}>
|
||||
{chartTypeLabels[type]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
{t('Overlays')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
{Object.values(Overlay).map((overlay) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={overlay}
|
||||
checked={overlays.includes(overlay)}
|
||||
onCheckedChange={() => {
|
||||
const newOverlays = [...overlays];
|
||||
const index = overlays.findIndex((item) => item === overlay);
|
||||
|
||||
index !== -1
|
||||
? newOverlays.splice(index, 1)
|
||||
: newOverlays.push(overlay);
|
||||
|
||||
setOverlays(newOverlays);
|
||||
}}
|
||||
>
|
||||
{overlayLabels[overlay]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
{t('Studies')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
{Object.values(Study).map((study) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={study}
|
||||
checked={studies.includes(study)}
|
||||
onCheckedChange={() => {
|
||||
const newStudies = [...studies];
|
||||
const index = studies.findIndex((item) => item === study);
|
||||
|
||||
index !== -1
|
||||
? newStudies.splice(index, 1)
|
||||
: newStudies.push(study);
|
||||
|
||||
setStudies(newStudies);
|
||||
}}
|
||||
>
|
||||
{studyLabels[study]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './__generated__/Candles';
|
||||
export * from './__generated__/Chart';
|
||||
export * from './candles-chart';
|
||||
export * from './candles-menu';
|
||||
export * from './data-source';
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
|
||||
import { ChartType, Interval, Study } from 'pennant';
|
||||
import { Overlay } from 'pennant';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
interface StoredSettings {
|
||||
interval: Interval;
|
||||
type: ChartType;
|
||||
overlays: Overlay[];
|
||||
studies: Study[];
|
||||
}
|
||||
|
||||
const DEFAULT_CHART_SETTINGS = {
|
||||
interval: Interval.I15M,
|
||||
type: ChartType.CANDLE,
|
||||
overlays: [],
|
||||
studies: [Study.VOLUME],
|
||||
};
|
||||
|
||||
export const useCandlesChartSettingsStore = create<
|
||||
StoredSettings & {
|
||||
merge: (settings: Partial<StoredSettings>) => void;
|
||||
setType: (type: ChartType) => void;
|
||||
setInterval: (interval: Interval) => void;
|
||||
setOverlays: (overlays: Overlay[]) => void;
|
||||
setStudies: (studies: Study[]) => void;
|
||||
}
|
||||
>()(
|
||||
persist(
|
||||
immer((set) => ({
|
||||
...DEFAULT_CHART_SETTINGS,
|
||||
merge: (settings: Partial<StoredSettings>) =>
|
||||
set((state) => {
|
||||
Object.assign(state, settings);
|
||||
}),
|
||||
setType: (type) =>
|
||||
set((state) => {
|
||||
state.type = type;
|
||||
}),
|
||||
setInterval: (interval) =>
|
||||
set((state) => {
|
||||
state.interval = interval;
|
||||
}),
|
||||
setOverlays: (overlays) =>
|
||||
set((state) => {
|
||||
state.overlays = overlays;
|
||||
}),
|
||||
setStudies: (studies) =>
|
||||
set((state) => {
|
||||
state.studies = studies;
|
||||
}),
|
||||
})),
|
||||
{
|
||||
name: 'vega_candles_chart_store',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const useCandlesChartSettings = () => {
|
||||
const settings = useCandlesChartSettingsStore();
|
||||
|
||||
const interval: Interval = getValidItem(
|
||||
settings.interval,
|
||||
Object.values(Interval),
|
||||
Interval.I15M
|
||||
);
|
||||
|
||||
const chartType: ChartType = getValidItem(
|
||||
settings.type,
|
||||
Object.values(ChartType),
|
||||
ChartType.CANDLE
|
||||
);
|
||||
|
||||
const overlays: Overlay[] = getValidSubset(
|
||||
settings.overlays,
|
||||
Object.values(Overlay),
|
||||
[]
|
||||
);
|
||||
|
||||
const studies: Study[] = getValidSubset(
|
||||
settings.studies,
|
||||
Object.values(Study),
|
||||
[Study.VOLUME]
|
||||
);
|
||||
|
||||
return {
|
||||
interval,
|
||||
chartType,
|
||||
overlays,
|
||||
studies,
|
||||
setInterval: settings.setInterval,
|
||||
setType: settings.setType,
|
||||
setStudies: settings.setStudies,
|
||||
setOverlays: settings.setOverlays,
|
||||
merge: settings.merge,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
export function createLog(name: string) {
|
||||
return (message: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}]: ${message}`);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export function waitForProposal(id: string): Promise<{ id: string }> {
|
||||
resolve(res.proposal);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
tick++;
|
||||
|
||||
@@ -15,7 +15,6 @@ export const addImportNodeWallets = () => {
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const obj = JSON.parse(result);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(obj);
|
||||
cy.writeFile(
|
||||
'./src/fixtures/wallet/node0RecoveryPhrase',
|
||||
|
||||
@@ -30,7 +30,6 @@ export const addValidatorsSelfDelegate = () => {
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const obj = JSON.parse(result);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(obj);
|
||||
cy.writeFile(
|
||||
'./src/fixtures/wallet/node0RecoveryPhrase',
|
||||
|
||||
@@ -30,10 +30,8 @@ export function addVegaWalletTopUpRewardsPool() {
|
||||
transferStartEpoch = Number(epochText.replace('Epoch', '')) + 5;
|
||||
transferEndEpoch = transferStartEpoch + 100;
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(transferStartEpoch);
|
||||
console.log(transferEndEpoch);
|
||||
/* eslint-enable */
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -9,14 +9,12 @@ export class CustomizedBridge extends Eip1193Bridge {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async sendAsync(...args: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('sendAsync called', ...args);
|
||||
return this.send(...args);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override async send(...args: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('send called', ...args);
|
||||
const isCallbackForm =
|
||||
typeof args[0] === 'object' && typeof args[1] === 'function';
|
||||
@@ -91,7 +89,6 @@ export class CustomizedBridge extends Eip1193Bridge {
|
||||
// All other transactions the base class works for
|
||||
result = await super.send(method, params);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('result received', method, params, result);
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result });
|
||||
@@ -99,7 +96,6 @@ export class CustomizedBridge extends Eip1193Bridge {
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
if (isCallbackForm) {
|
||||
callback(error, null);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
|
||||
interface MarketNameCellProps {
|
||||
value?: string | null;
|
||||
value?: string;
|
||||
data?: { id?: string; marketId?: string; market?: { id: string } };
|
||||
idPath?: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Control } from 'react-hook-form';
|
||||
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -9,8 +9,7 @@ import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||
export interface DealTicketAmountProps {
|
||||
control: Control<OrderFormFields>;
|
||||
orderType: Schema.OrderType;
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string;
|
||||
marketData: MarketData;
|
||||
market: Market;
|
||||
sizeError?: string;
|
||||
priceError?: string;
|
||||
@@ -22,18 +21,11 @@ export interface DealTicketAmountProps {
|
||||
export const DealTicketAmount = ({
|
||||
orderType,
|
||||
marketData,
|
||||
marketPrice,
|
||||
...props
|
||||
}: DealTicketAmountProps) => {
|
||||
switch (orderType) {
|
||||
case Schema.OrderType.TYPE_MARKET:
|
||||
return (
|
||||
<DealTicketMarketAmount
|
||||
{...props}
|
||||
marketData={marketData}
|
||||
marketPrice={marketPrice}
|
||||
/>
|
||||
);
|
||||
return <DealTicketMarketAmount {...props} marketData={marketData} />;
|
||||
case Schema.OrderType.TYPE_LIMIT:
|
||||
return <DealTicketLimitAmount {...props} />;
|
||||
default: {
|
||||
|
||||
@@ -4,10 +4,9 @@ import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
side: Side;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const DealTicketButton = ({ side, label }: Props) => {
|
||||
export const DealTicketButton = ({ side }: Props) => {
|
||||
const buttonClasses = classNames(
|
||||
'px-10 py-2 uppercase rounded-md text-white w-full',
|
||||
{
|
||||
@@ -18,7 +17,7 @@ export const DealTicketButton = ({ side, label }: Props) => {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<button type="submit" data-testid="place-order" className={buttonClasses}>
|
||||
{label || t('Place order')}
|
||||
{t('Place order')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { StopOrder } from './deal-ticket-stop-order';
|
||||
import {
|
||||
useStaticMarketData,
|
||||
useMarket,
|
||||
useMarketPrice,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
import { useMarket, marketDataProvider } from '@vegaprotocol/markets';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
interface DealTicketContainerProps {
|
||||
export interface DealTicketContainerProps {
|
||||
marketId: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
onClickCollateral?: () => void;
|
||||
@@ -23,9 +14,10 @@ interface DealTicketContainerProps {
|
||||
|
||||
export const DealTicketContainer = ({
|
||||
marketId,
|
||||
...props
|
||||
onMarketClick,
|
||||
onClickCollateral,
|
||||
onDeposit,
|
||||
}: DealTicketContainerProps) => {
|
||||
const type = useDealTicketTypeStore((state) => state.type[marketId]);
|
||||
const {
|
||||
data: market,
|
||||
error: marketError,
|
||||
@@ -37,9 +29,15 @@ export const DealTicketContainer = ({
|
||||
error: marketDataError,
|
||||
loading: marketDataLoading,
|
||||
reload,
|
||||
} = useStaticMarketData(marketId);
|
||||
const { data: marketPrice } = useMarketPrice(market?.id);
|
||||
} = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketDataProvider,
|
||||
variables: { marketId },
|
||||
},
|
||||
1000
|
||||
);
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
data={market && marketData}
|
||||
@@ -48,23 +46,14 @@ export const DealTicketContainer = ({
|
||||
reload={reload}
|
||||
>
|
||||
{market && marketData ? (
|
||||
FLAGS.STOP_ORDERS &&
|
||||
(type === DealTicketType.StopLimit ||
|
||||
type === DealTicketType.StopMarket) ? (
|
||||
<StopOrder
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
|
||||
/>
|
||||
) : (
|
||||
<DealTicket
|
||||
{...props}
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
/>
|
||||
)
|
||||
<DealTicket
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
onClickCollateral={onClickCollateral}
|
||||
onMarketClick={onMarketClick}
|
||||
onDeposit={onDeposit}
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
<p>{t('Could not load market')}</p>
|
||||
|
||||
@@ -4,11 +4,11 @@ import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FeesBreakdown } from '@vegaprotocol/markets';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
|
||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||
|
||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import { useEstimateFees } from '../../hooks';
|
||||
|
||||
const emptyValue = '-';
|
||||
|
||||
@@ -77,82 +76,26 @@ export const DealTicketFeeDetail = ({
|
||||
};
|
||||
|
||||
export interface DealTicketFeeDetailsProps {
|
||||
assetSymbol: string;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
market: Market;
|
||||
notionalSize: string | null;
|
||||
}
|
||||
|
||||
export const DealTicketFeeDetails = ({
|
||||
assetSymbol,
|
||||
order,
|
||||
market,
|
||||
notionalSize,
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const feeEstimate = useEstimateFees(order);
|
||||
const { settlementAsset: asset } =
|
||||
market.tradableInstrument.instrument.product;
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
const marketDecimals = market.decimalPlaces;
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Notional')}
|
||||
value={formatValue(notionalSize, marketDecimals)}
|
||||
formattedValue={formatValue(notionalSize, marketDecimals)}
|
||||
symbol={quoteName}
|
||||
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
||||
/>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Fees')}
|
||||
value={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
<span>
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
||||
)}
|
||||
</span>
|
||||
<FeesBreakdown
|
||||
fees={feeEstimate?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface DealTicketMarginDetailsProps {
|
||||
generalAccountBalance?: string;
|
||||
marginAccountBalance?: string;
|
||||
market: Market;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
assetSymbol: string;
|
||||
notionalSize: string | null;
|
||||
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
|
||||
positionEstimate: EstimatePositionQuery['estimatePosition'];
|
||||
}
|
||||
|
||||
export const DealTicketMarginDetails = ({
|
||||
export const DealTicketFeeDetails = ({
|
||||
marginAccountBalance,
|
||||
generalAccountBalance,
|
||||
assetSymbol,
|
||||
feeEstimate,
|
||||
market,
|
||||
onMarketClick,
|
||||
notionalSize,
|
||||
positionEstimate,
|
||||
}: DealTicketMarginDetailsProps) => {
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
@@ -167,6 +110,7 @@ export const DealTicketMarginDetails = ({
|
||||
const { settlementAsset: asset } =
|
||||
market.tradableInstrument.instrument.product;
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
const marketDecimals = market.decimalPlaces;
|
||||
let marginRequiredBestCase: string | undefined = undefined;
|
||||
let marginRequiredWorstCase: string | undefined = undefined;
|
||||
if (marginEstimate) {
|
||||
@@ -307,7 +251,41 @@ export const DealTicketMarginDetails = ({
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Notional')}
|
||||
value={formatValue(notionalSize, marketDecimals)}
|
||||
formattedValue={formatValue(notionalSize, marketDecimals)}
|
||||
symbol={quoteName}
|
||||
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
||||
/>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Fees')}
|
||||
value={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
<span>
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
||||
)}
|
||||
</span>
|
||||
<FeesBreakdown
|
||||
fees={feeEstimate?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Margin required')}
|
||||
value={formatRange(
|
||||
@@ -373,6 +351,6 @@ export const DealTicketMarginDetails = ({
|
||||
onClose={onAccountBreakdownDialogClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,12 +44,12 @@ export const DealTicketLimitAmount = ({
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<FormGroup
|
||||
label={t('Size')}
|
||||
labelFor="input-order-size-limit"
|
||||
className="!mb-0"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="size"
|
||||
@@ -78,13 +78,16 @@ export const DealTicketLimitAmount = ({
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div className="pt-7 leading-10">@</div>
|
||||
<div className="flex-0 items-center">
|
||||
<div className="flex"> </div>
|
||||
<div className="flex">@</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<FormGroup
|
||||
labelFor="input-price-quote"
|
||||
label={t(`Price (${quoteName})`)}
|
||||
labelAlign="right"
|
||||
className="!mb-0"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="price"
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { isMarketInAuction } from '@vegaprotocol/markets';
|
||||
import { isMarketInAuction } from '../../utils';
|
||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { getMarketPrice } from '../../utils/get-price';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export type DealTicketMarketAmountProps = Omit<
|
||||
DealTicketAmountProps,
|
||||
@@ -19,26 +19,37 @@ export const DealTicketMarketAmount = ({
|
||||
control,
|
||||
market,
|
||||
marketData,
|
||||
marketPrice,
|
||||
sizeError,
|
||||
update,
|
||||
size,
|
||||
}: DealTicketMarketAmountProps) => {
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
const price = marketPrice;
|
||||
const price = getMarketPrice(marketData);
|
||||
|
||||
const priceFormatted = price
|
||||
? addDecimalsFormatNumber(price, market.decimalPlaces)
|
||||
: undefined;
|
||||
|
||||
const inAuction = isMarketInAuction(marketData.marketTradingMode);
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-end gap-4 mb-2">
|
||||
<div className="flex-1 text-sm">{t('Size')}</div>
|
||||
<div />
|
||||
<div className="flex-2 text-sm text-right">
|
||||
{isMarketInAuction(marketData.marketTradingMode) && (
|
||||
<Tooltip
|
||||
description={t(
|
||||
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
|
||||
)}
|
||||
>
|
||||
<div>{t(`Indicative price`)}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 text-sm">{t('Size')}</div>
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
@@ -65,29 +76,15 @@ export const DealTicketMarketAmount = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-7 leading-10">@</div>
|
||||
<div className="flex-1 text-sm text-right">
|
||||
{inAuction && (
|
||||
<Tooltip
|
||||
description={t(
|
||||
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
|
||||
)}
|
||||
>
|
||||
<div className="mb-2">{t(`Indicative price`)}</div>
|
||||
</Tooltip>
|
||||
<div>@</div>
|
||||
<div className="flex-1 text-sm text-right" data-testid="last-price">
|
||||
{priceFormatted && quoteName ? (
|
||||
<>
|
||||
~{priceFormatted} {quoteName}
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
<div
|
||||
data-testid="last-price"
|
||||
className={classNames('leading-10', { 'pt-7': !inAuction })}
|
||||
>
|
||||
{priceFormatted && quoteName ? (
|
||||
<>
|
||||
~{priceFormatted} {quoteName}
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{sizeError && (
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { generateMarket } from '../../test-helpers';
|
||||
import { StopOrder } from './deal-ticket-stop-order';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
import type { FeatureFlags } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('zustand');
|
||||
jest.mock('./deal-ticket-fee-details', () => ({
|
||||
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => {
|
||||
const actual = jest.requireActual('@vegaprotocol/environment');
|
||||
return {
|
||||
...actual,
|
||||
FLAGS: {
|
||||
...actual.FLAGS,
|
||||
STOP_ORDERS: true,
|
||||
} as FeatureFlags,
|
||||
};
|
||||
});
|
||||
|
||||
const marketPrice = '200';
|
||||
const market = generateMarket();
|
||||
const submit = jest.fn();
|
||||
|
||||
function generateJsx(pubKey: string | null = 'pubKey', isReadOnly = false) {
|
||||
return (
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={{ pubKey, isReadOnly } as any}>
|
||||
<StopOrder market={market} marketPrice={marketPrice} submit={submit} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const submitButton = 'place-order';
|
||||
const sizeInput = 'order-size';
|
||||
const priceInput = 'order-price';
|
||||
const triggerPriceInput = 'triggerPrice';
|
||||
const triggerTrailingPercentOffsetInput = 'triggerTrailingPercentOffset';
|
||||
|
||||
const orderTypeTrigger = 'order-type-Stop';
|
||||
const orderTypeLimit = 'order-type-StopLimit';
|
||||
const orderTypeMarket = 'order-type-StopMarket';
|
||||
|
||||
const orderSideBuy = 'order-side-SIDE_BUY';
|
||||
const orderSideSell = 'order-side-SIDE_SELL';
|
||||
|
||||
const triggerDirectionRisesAbove = 'triggerDirection-risesAbove';
|
||||
// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
|
||||
|
||||
const expiryStrategySubmit = 'expiryStrategy-submit';
|
||||
const expiryStrategyCancel = 'expiryStrategy-cancel';
|
||||
|
||||
const triggerTypePrice = 'triggerType-price';
|
||||
const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset';
|
||||
|
||||
const expire = 'expire';
|
||||
const datePicker = 'date-picker-field';
|
||||
const timeInForce = 'order-tif';
|
||||
|
||||
const sizeErrorMessage = 'stop-order-error-message-size';
|
||||
const priceErrorMessage = 'stop-order-error-message-price';
|
||||
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
|
||||
const triggerTrailingPercentOffsetErrorMessage =
|
||||
'stop-order-error-message-trigger-trailing-percent-offset';
|
||||
|
||||
describe('StopOrder', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should display ticket defaults', async () => {
|
||||
render(generateJsx());
|
||||
// place order button should always be enabled
|
||||
expect(screen.getByTestId(submitButton)).toBeEnabled();
|
||||
|
||||
// Assert defaults are used
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
expect(screen.getByTestId(orderTypeLimit).dataset.state).toEqual('checked');
|
||||
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||
expect(screen.getByTestId(orderSideBuy).dataset.state).toEqual('checked');
|
||||
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue('0');
|
||||
expect(screen.getByTestId(timeInForce)).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
|
||||
).toEqual('checked');
|
||||
expect(screen.getByTestId(triggerTypePrice).dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
|
||||
await userEvent.click(screen.getByTestId(expire));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display trigger price as price for market type order', async () => {
|
||||
render(generateJsx());
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '10');
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('10.0');
|
||||
});
|
||||
|
||||
it('should use local storage state for initial values', async () => {
|
||||
const values: Partial<StopOrderFormValues> = {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '0.1',
|
||||
price: '300.22',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
expire: true,
|
||||
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
|
||||
expiresAt: '2023-07-27T16:43:27.000',
|
||||
};
|
||||
|
||||
useStopOrderFormValues.setState({
|
||||
formValues: {
|
||||
[market.id]: values,
|
||||
},
|
||||
});
|
||||
|
||||
render(generateJsx());
|
||||
// Assert correct defaults are used from store
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
expect(screen.queryByTestId(orderTypeLimit)).toBeChecked();
|
||||
expect(screen.getByTestId(orderSideSell).dataset.state).toEqual('checked');
|
||||
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue(
|
||||
values.size as string
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce);
|
||||
expect(screen.getByTestId(priceInput)).toHaveDisplayValue(
|
||||
values.price as string
|
||||
);
|
||||
expect(screen.getByTestId(expire).dataset.state).toEqual('checked');
|
||||
expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId(datePicker)).toHaveDisplayValue(
|
||||
values.expiresAt as string
|
||||
);
|
||||
});
|
||||
|
||||
it('shows no wallet warning and do not submit if no wallet connected', async () => {
|
||||
render(generateJsx(null));
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '1');
|
||||
await userEvent.type(screen.getByTestId(priceInput), '1');
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
expect(submit).not.toBeCalled();
|
||||
expect(
|
||||
screen.getByTestId('deal-ticket-connect-wallet')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls submit if form is valid', async () => {
|
||||
render(generateJsx());
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '1');
|
||||
await userEvent.type(screen.getByTestId(priceInput), '1');
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
expect(submit).toBeCalled();
|
||||
});
|
||||
|
||||
it('validates size field', async () => {
|
||||
render(generateJsx());
|
||||
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
|
||||
// default value should be invalid
|
||||
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||
// to small value should be invalid
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
|
||||
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// clear and fill using valid value
|
||||
await userEvent.clear(screen.getByTestId(sizeInput));
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
|
||||
});
|
||||
|
||||
it('validates price field', async () => {
|
||||
render(generateJsx());
|
||||
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
// price error message should not show if size has error
|
||||
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// switch to market order type error should disappear
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||
|
||||
// switch back to limit type
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// to small value should be invalid
|
||||
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// clear and fill using valid value
|
||||
await userEvent.clear(screen.getByTestId(priceInput));
|
||||
await userEvent.type(screen.getByTestId(priceInput), '0.01');
|
||||
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||
});
|
||||
|
||||
it('validates trigger price field', async () => {
|
||||
render(generateJsx());
|
||||
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// switch to trailing percentage offset trigger type
|
||||
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
|
||||
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||
|
||||
// switch back to price trigger type
|
||||
await userEvent.click(screen.getByTestId(triggerTypePrice));
|
||||
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// to small value should be invalid
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
|
||||
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// clear and fill using valid value
|
||||
await userEvent.clear(screen.getByTestId(triggerPriceInput));
|
||||
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
|
||||
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||
});
|
||||
|
||||
it('validates trigger trailing percentage offset field', async () => {
|
||||
render(generateJsx());
|
||||
|
||||
// should not show error with default form values
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
expect(
|
||||
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||
).toBeNull();
|
||||
|
||||
// switch to trailing percentage offset trigger type
|
||||
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
|
||||
expect(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// to small value should be invalid
|
||||
await userEvent.type(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||
'0.09'
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// clear and fill using valid value
|
||||
await userEvent.clear(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||
);
|
||||
await userEvent.type(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||
'0.1'
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||
).toBeNull();
|
||||
|
||||
// to big value should be invalid
|
||||
await userEvent.clear(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||
);
|
||||
await userEvent.type(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||
'99.91'
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// clear and fill using valid value
|
||||
await userEvent.clear(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||
);
|
||||
await userEvent.type(
|
||||
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||
'99.9'
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,590 +0,0 @@
|
||||
import type { FormEventHandler } from 'react';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
formatNumber,
|
||||
removeDecimal,
|
||||
toDecimal,
|
||||
validateAmount,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Input,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
InputError,
|
||||
Select,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
|
||||
import {
|
||||
NoWalletWarning,
|
||||
REDUCE_ONLY_TOOLTIP,
|
||||
useNotionalSize,
|
||||
} from './deal-ticket';
|
||||
import { TypeToggle } from './type-selector';
|
||||
import {
|
||||
useStopOrderFormValues,
|
||||
type StopOrderFormValues,
|
||||
} from '../../hooks/use-stop-order-form-values';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
import { validateExpiration } from '../../utils';
|
||||
|
||||
export interface StopOrderProps {
|
||||
market: Market;
|
||||
marketPrice?: string | null;
|
||||
submit: (order: StopOrdersSubmission) => void;
|
||||
}
|
||||
|
||||
const defaultValues: Partial<StopOrderFormValues> = {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
triggerType: 'price',
|
||||
triggerDirection:
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||
size: '0',
|
||||
};
|
||||
|
||||
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
|
||||
|
||||
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||
const [, updateOrder] = useOrder(market.id);
|
||||
const updateStoredFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
);
|
||||
const storedFormValues = useStopOrderFormValues(
|
||||
(state) => state.formValues[market.id]
|
||||
);
|
||||
const { handleSubmit, setValue, watch, control, formState } =
|
||||
useForm<StopOrderFormValues>({
|
||||
defaultValues: { ...defaultValues, ...storedFormValues },
|
||||
});
|
||||
const { errors } = formState;
|
||||
const lastSubmitTime = useRef(0);
|
||||
const onSubmit = useCallback(
|
||||
(data: StopOrderFormValues) => {
|
||||
const now = new Date().getTime();
|
||||
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||
return;
|
||||
}
|
||||
submit(
|
||||
mapFormValuesToStopOrdersSubmission(
|
||||
data,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
lastSubmitTime.current = now;
|
||||
},
|
||||
[market.id, market.decimalPlaces, market.positionDecimalPlaces, submit]
|
||||
);
|
||||
const side = watch('side');
|
||||
const expire = watch('expire');
|
||||
const triggerType = watch('triggerType');
|
||||
const triggerPrice = watch('triggerPrice');
|
||||
const timeInForce = watch('timeInForce');
|
||||
const type = watch('type');
|
||||
const rawPrice = watch('price');
|
||||
const rawSize = watch('size');
|
||||
|
||||
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
|
||||
setValue('size', storedFormValues.size);
|
||||
}
|
||||
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
|
||||
setValue('price', storedFormValues.price);
|
||||
}
|
||||
|
||||
const isPriceTrigger = triggerType === 'price';
|
||||
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
|
||||
const price =
|
||||
marketPrice &&
|
||||
getDerivedPrice(
|
||||
{
|
||||
type,
|
||||
price: rawPrice && removeDecimal(rawPrice, market.decimalPlaces),
|
||||
},
|
||||
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
|
||||
? removeDecimal(triggerPrice, market.decimalPlaces)
|
||||
: marketPrice
|
||||
);
|
||||
|
||||
const notionalSize = useNotionalSize(
|
||||
price,
|
||||
size,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch((value, { name, type }) => {
|
||||
updateStoredFormValues(market.id, value);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch, market.id, updateStoredFormValues]);
|
||||
|
||||
const { quoteName, settlementAsset: asset } =
|
||||
market.tradableInstrument.instrument.product;
|
||||
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const trailingPercentOffsetStep = '0.1';
|
||||
|
||||
const priceFormatted =
|
||||
isPriceTrigger && triggerPrice
|
||||
? formatNumber(triggerPrice, market.decimalPlaces)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
|
||||
noValidate
|
||||
>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { value } = field;
|
||||
return (
|
||||
<TypeToggle
|
||||
value={
|
||||
value === Schema.OrderType.TYPE_LIMIT
|
||||
? DealTicketType.StopLimit
|
||||
: DealTicketType.StopMarket
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
const type = value as DealTicketType;
|
||||
setDealTicketType(market.id, type);
|
||||
if (
|
||||
type === DealTicketType.Limit ||
|
||||
type === DealTicketType.Market
|
||||
) {
|
||||
updateOrder({
|
||||
type:
|
||||
type === DealTicketType.Limit
|
||||
? Schema.OrderType.TYPE_LIMIT
|
||||
: Schema.OrderType.TYPE_MARKET,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setValue(
|
||||
'type',
|
||||
type === DealTicketType.StopLimit
|
||||
? Schema.OrderType.TYPE_LIMIT
|
||||
: Schema.OrderType.TYPE_MARKET
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.type && (
|
||||
<InputError testId="stop-order-error-message-type">
|
||||
{errors.type.message}
|
||||
</InputError>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="side"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
<FormGroup label={t('Trigger')} compact={true} labelFor="">
|
||||
<Controller
|
||||
name="triggerDirection"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<RadioGroup
|
||||
name="triggerDirection"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
className="mb-2"
|
||||
>
|
||||
<Radio
|
||||
value={
|
||||
Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
}
|
||||
id="triggerDirection-risesAbove"
|
||||
label={'Rises above'}
|
||||
/>
|
||||
<Radio
|
||||
value={
|
||||
Schema.StopOrderTriggerDirection
|
||||
.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
}
|
||||
id="triggerDirection-fallsBelow"
|
||||
label={'Falls below'}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{isPriceTrigger && (
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="triggerPrice"
|
||||
rules={{
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
data-testid="triggerPrice"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
appendElement={asset.symbol}
|
||||
value={value || ''}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.triggerPrice && (
|
||||
<InputError testId="stop-order-error-message-trigger-price">
|
||||
{errors.triggerPrice.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isPriceTrigger && (
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="triggerTrailingPercentOffset"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a trailing percent offset'),
|
||||
min: {
|
||||
value: trailingPercentOffsetStep,
|
||||
message: t(
|
||||
'Trailing percent offset cannot be lower than ' +
|
||||
trailingPercentOffsetStep
|
||||
),
|
||||
},
|
||||
max: {
|
||||
value: '99.9',
|
||||
message: t(
|
||||
'Trailing percent offset cannot be higher than 99.9'
|
||||
),
|
||||
},
|
||||
validate: validateAmount(
|
||||
trailingPercentOffsetStep,
|
||||
'Trailing percentage offset'
|
||||
),
|
||||
}}
|
||||
render={({ field }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
type="number"
|
||||
step={trailingPercentOffsetStep}
|
||||
appendElement="%"
|
||||
data-testid="triggerTrailingPercentOffset"
|
||||
value={value || ''}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.triggerTrailingPercentOffset && (
|
||||
<InputError testId="stop-order-error-message-trigger-trailing-percent-offset">
|
||||
{errors.triggerTrailingPercentOffset.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
name="triggerType"
|
||||
control={control}
|
||||
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
|
||||
render={({ field }) => {
|
||||
const { onChange, value } = field;
|
||||
return (
|
||||
<RadioGroup
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<Radio value="price" id="triggerType-price" label={'Price'} />
|
||||
<Radio
|
||||
value="trailingPercentOffset"
|
||||
id="triggerType-trailingPercentOffset"
|
||||
label={'Trailing Percent Offset'}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<div className="mb-2">
|
||||
<div className="flex items-start gap-4">
|
||||
<FormGroup
|
||||
labelFor="input-price-quote"
|
||||
label={t(`Size`)}
|
||||
className="!mb-0 flex-1"
|
||||
>
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<Input
|
||||
id="order-size"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
value={value || ''}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<div className="pt-7 leading-10">@</div>
|
||||
<div className="flex-1">
|
||||
{type === Schema.OrderType.TYPE_LIMIT ? (
|
||||
<FormGroup
|
||||
labelFor="input-price-quote"
|
||||
label={t(`Price (${quoteName})`)}
|
||||
labelAlign="right"
|
||||
className="!mb-0"
|
||||
>
|
||||
<Controller
|
||||
name="price"
|
||||
control={control}
|
||||
rules={{
|
||||
deps: 'type',
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
render={({ field }) => {
|
||||
const { value, ...props } = field;
|
||||
return (
|
||||
<Input
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
value={value || ''}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : (
|
||||
<div
|
||||
className="text-sm text-right pt-7 leading-10"
|
||||
data-testid="price"
|
||||
>
|
||||
{priceFormatted && quoteName
|
||||
? `~${priceFormatted} ${quoteName}`
|
||||
: '-'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{errors.size && (
|
||||
<InputError testId="stop-order-error-message-size">
|
||||
{errors.size.message}
|
||||
</InputError>
|
||||
)}
|
||||
|
||||
{!errors.size &&
|
||||
errors.price &&
|
||||
type === Schema.OrderType.TYPE_LIMIT && (
|
||||
<InputError testId="stop-order-error-message-price">
|
||||
{errors.price.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<FormGroup
|
||||
label={t('Time in force')}
|
||||
labelFor="select-time-in-force"
|
||||
compact={true}
|
||||
>
|
||||
<Controller
|
||||
name="timeInForce"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
id="select-time-in-force"
|
||||
className="w-full"
|
||||
data-testid="order-tif"
|
||||
{...field}
|
||||
>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
|
||||
</option>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
|
||||
</option>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{errors.timeInForce && (
|
||||
<InputError testId="stop-error-message-tif">
|
||||
{errors.timeInForce.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 pb-2 justify-end">
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
label={
|
||||
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
|
||||
<span className="text-xs">{t('Reduce only')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="expire"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: onCheckedChange, value } = field;
|
||||
return (
|
||||
<Checkbox
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={value}
|
||||
name="expire"
|
||||
label={'Expire'}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{expire && (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t('Strategy')}
|
||||
labelFor="expiryStrategy"
|
||||
compact={true}
|
||||
>
|
||||
<Controller
|
||||
name="expiryStrategy"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<RadioGroup orientation="horizontal" {...field}>
|
||||
<Radio
|
||||
value={
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||
}
|
||||
id="expiryStrategy-submit"
|
||||
label={'Submit'}
|
||||
/>
|
||||
<Radio
|
||||
value={
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||
}
|
||||
id="expiryStrategy-cancel"
|
||||
label={'Cancel'}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="expiresAt"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: validateExpiration,
|
||||
}}
|
||||
render={({ field }) => {
|
||||
const { value, onChange: onSelect } = field;
|
||||
return (
|
||||
<ExpirySelector
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
errorMessage={errors.expiresAt?.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<NoWalletWarning pubKey={pubKey} isReadOnly={isReadOnly} asset={asset} />
|
||||
<DealTicketButton side={side} label={t('Submit Stop Order')} />
|
||||
<DealTicketFeeDetails
|
||||
order={{
|
||||
marketId: market.id,
|
||||
price: price || undefined,
|
||||
side,
|
||||
size,
|
||||
timeInForce,
|
||||
type,
|
||||
}}
|
||||
notionalSize={notionalSize}
|
||||
assetSymbol={asset.symbol}
|
||||
market={market}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -22,12 +22,8 @@ import { OrdersDocument } from '@vegaprotocol/orders';
|
||||
jest.mock('zustand');
|
||||
jest.mock('./deal-ticket-fee-details', () => ({
|
||||
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||
DealTicketMarginDetails: () => (
|
||||
<div data-testid="deal-ticket-margin-details" />
|
||||
),
|
||||
}));
|
||||
|
||||
const marketPrice = '200';
|
||||
const pubKey = 'pubKey';
|
||||
const market = generateMarket();
|
||||
const marketData = generateMarketData();
|
||||
@@ -40,7 +36,6 @@ function generateJsx(mocks: MockedResponse[] = []) {
|
||||
<DealTicket
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
marketPrice={marketPrice}
|
||||
submit={submit}
|
||||
onDeposit={jest.fn()}
|
||||
/>
|
||||
@@ -119,22 +114,30 @@ describe('DealTicket', () => {
|
||||
});
|
||||
|
||||
it('should display ticket defaults', () => {
|
||||
render(generateJsx());
|
||||
const { container } = render(generateJsx());
|
||||
|
||||
// place order button should always be enabled
|
||||
expect(screen.getByTestId('place-order')).toBeEnabled();
|
||||
|
||||
// Assert defaults are used
|
||||
expect(screen.getByTestId('order-type-Market')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('order-type-Limit')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||
'checked'
|
||||
const oderTypeLimitToggle = container.querySelector(
|
||||
`[data-testid="order-type-${Schema.OrderType.TYPE_LIMIT}"] input[type="radio"]`
|
||||
);
|
||||
expect(oderTypeLimitToggle).toBeChecked();
|
||||
|
||||
expect(screen.getByTestId('order-side-SIDE_BUY').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||
@@ -144,12 +147,12 @@ describe('DealTicket', () => {
|
||||
it('should display last price for market type order', () => {
|
||||
render(generateJsx());
|
||||
act(() => {
|
||||
screen.getByTestId('order-type-Market').click();
|
||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`).click();
|
||||
});
|
||||
// Assert last price is shown
|
||||
expect(screen.getByTestId('last-price')).toHaveTextContent(
|
||||
// eslint-disable-next-line
|
||||
`~${addDecimal(marketPrice, market.decimalPlaces)} ${
|
||||
`~${addDecimal(marketData.markPrice, market.decimalPlaces)} ${
|
||||
market.tradableInstrument.instrument.product.quoteName
|
||||
}`
|
||||
);
|
||||
@@ -175,12 +178,17 @@ describe('DealTicket', () => {
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
@@ -213,12 +221,17 @@ describe('DealTicket', () => {
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
@@ -256,12 +269,17 @@ describe('DealTicket', () => {
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
@@ -304,12 +322,17 @@ describe('DealTicket', () => {
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
@@ -348,12 +371,17 @@ describe('DealTicket', () => {
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
@@ -374,7 +402,7 @@ describe('DealTicket', () => {
|
||||
render(generateJsx());
|
||||
|
||||
act(() => {
|
||||
screen.getByTestId('order-type-Market').click();
|
||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`).click();
|
||||
});
|
||||
|
||||
// Only FOK and IOC should be present for type market order
|
||||
@@ -399,7 +427,7 @@ describe('DealTicket', () => {
|
||||
);
|
||||
|
||||
// Switch to type limit order -> all TIF options should be shown
|
||||
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||
Object.keys(Schema.OrderTimeInForce).length
|
||||
);
|
||||
@@ -419,7 +447,7 @@ describe('DealTicket', () => {
|
||||
);
|
||||
|
||||
// Switch back to type market order -> FOK should be preserved from previous selection
|
||||
await userEvent.click(screen.getByTestId('order-type-Market'));
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||
);
|
||||
@@ -434,7 +462,7 @@ describe('DealTicket', () => {
|
||||
);
|
||||
|
||||
// Switch back type limit order -> GTT should be preserved
|
||||
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
@@ -449,7 +477,7 @@ describe('DealTicket', () => {
|
||||
);
|
||||
|
||||
// Switch to type market order -> IOC should be preserved
|
||||
await userEvent.click(screen.getByTestId('order-type-Market'));
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
@@ -459,9 +487,9 @@ describe('DealTicket', () => {
|
||||
render(generateJsx());
|
||||
|
||||
// BUY is selected by default
|
||||
expect(screen.getByTestId('order-side-SIDE_BUY').dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
|
||||
await userEvent.type(screen.getByTestId('order-size'), '200');
|
||||
|
||||
@@ -476,7 +504,7 @@ describe('DealTicket', () => {
|
||||
);
|
||||
|
||||
// Switch to limit order
|
||||
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
|
||||
// Check all TIF options shown
|
||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||
|
||||
@@ -4,10 +4,7 @@ import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { DealTicketAmount } from './deal-ticket-amount';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import {
|
||||
DealTicketFeeDetails,
|
||||
DealTicketMarginDetails,
|
||||
} from './deal-ticket-fee-details';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
@@ -33,7 +30,8 @@ import {
|
||||
} from '@vegaprotocol/positions';
|
||||
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
||||
import { getDerivedPrice } from '../../utils/get-price';
|
||||
import type { OrderInfo } from '@vegaprotocol/types';
|
||||
|
||||
import {
|
||||
@@ -45,11 +43,7 @@ import {
|
||||
} from '../../utils';
|
||||
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
||||
import { SummaryValidationType } from '../../constants';
|
||||
import type {
|
||||
Market,
|
||||
MarketData,
|
||||
StaticMarketData,
|
||||
} from '@vegaprotocol/markets';
|
||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||
import {
|
||||
useMarketAccountBalance,
|
||||
@@ -59,59 +53,26 @@ import {
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { useOrderForm } from '../../hooks/use-order-form';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
export const REDUCE_ONLY_TOOLTIP =
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||
|
||||
export interface DealTicketProps {
|
||||
market: Market;
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string | null;
|
||||
marketData: MarketData;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
submit: (order: OrderSubmission) => void;
|
||||
onClickCollateral?: () => void;
|
||||
onDeposit: (assetId: string) => void;
|
||||
}
|
||||
|
||||
export const useNotionalSize = (
|
||||
price: string | null | undefined,
|
||||
size: string | undefined,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
) =>
|
||||
useMemo(() => {
|
||||
if (price && size) {
|
||||
return removeDecimal(
|
||||
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
||||
toBigNum(price, decimalPlaces)
|
||||
),
|
||||
decimalPlaces
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [price, size, decimalPlaces, positionDecimalPlaces]);
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
onMarketClick,
|
||||
marketData,
|
||||
marketPrice,
|
||||
submit,
|
||||
onClickCollateral,
|
||||
onDeposit,
|
||||
}: DealTicketProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||
const updateStopOrderFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
);
|
||||
// store last used tif for market so that when changing OrderType the previous TIF
|
||||
// selection for that type is used when switching back
|
||||
|
||||
@@ -134,15 +95,11 @@ export const DealTicket = ({
|
||||
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
|
||||
const {
|
||||
accountBalance: marginAccountBalance,
|
||||
loading: loadingMarginAccountBalance,
|
||||
} = useMarketAccountBalance(market.id);
|
||||
const { accountBalance: marginAccountBalance } = useMarketAccountBalance(
|
||||
market.id
|
||||
);
|
||||
|
||||
const {
|
||||
accountBalance: generalAccountBalance,
|
||||
loading: loadingGeneralAccountBalance,
|
||||
} = useAccountBalance(asset.id);
|
||||
const { accountBalance: generalAccountBalance } = useAccountBalance(asset.id);
|
||||
|
||||
const balance = (
|
||||
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
|
||||
@@ -159,20 +116,30 @@ export const DealTicket = ({
|
||||
);
|
||||
|
||||
const price = useMemo(() => {
|
||||
return (
|
||||
normalizedOrder &&
|
||||
marketPrice &&
|
||||
getDerivedPrice(normalizedOrder, marketPrice)
|
||||
);
|
||||
}, [normalizedOrder, marketPrice]);
|
||||
return normalizedOrder && getDerivedPrice(normalizedOrder, marketData);
|
||||
}, [normalizedOrder, marketData]);
|
||||
|
||||
const notionalSize = useNotionalSize(
|
||||
const notionalSize = useMemo(() => {
|
||||
if (price && normalizedOrder?.size) {
|
||||
return removeDecimal(
|
||||
toBigNum(
|
||||
normalizedOrder.size,
|
||||
market.positionDecimalPlaces
|
||||
).multipliedBy(toBigNum(price, market.decimalPlaces)),
|
||||
market.decimalPlaces
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
price,
|
||||
normalizedOrder?.size,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
market.positionDecimalPlaces,
|
||||
]);
|
||||
|
||||
const feeEstimate = useEstimateFees(
|
||||
normalizedOrder && { ...normalizedOrder, price }
|
||||
);
|
||||
const { data: activeOrders } = useDataProvider({
|
||||
dataProvider: activeOrdersProvider,
|
||||
variables: { partyId: pubKey || '', marketId: market.id },
|
||||
@@ -230,10 +197,7 @@ export const DealTicket = ({
|
||||
|
||||
const hasNoBalance =
|
||||
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
|
||||
if (
|
||||
hasNoBalance &&
|
||||
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
||||
) {
|
||||
if (hasNoBalance) {
|
||||
setError('summary', {
|
||||
message: SummaryValidationType.NoCollateral,
|
||||
type: SummaryValidationType.NoCollateral,
|
||||
@@ -257,8 +221,6 @@ export const DealTicket = ({
|
||||
marketTradingMode,
|
||||
generalAccountBalance,
|
||||
marginAccountBalance,
|
||||
loadingMarginAccountBalance,
|
||||
loadingGeneralAccountBalance,
|
||||
pubKey,
|
||||
setError,
|
||||
clearErrors,
|
||||
@@ -303,13 +265,11 @@ export const DealTicket = ({
|
||||
);
|
||||
|
||||
// if an order doesn't exist one will be created by the store immediately
|
||||
if (!order || !normalizedOrder) {
|
||||
return null;
|
||||
}
|
||||
if (!order || !normalizedOrder) return null;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
|
||||
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
|
||||
noValidate
|
||||
data-testid="deal-ticket-form"
|
||||
>
|
||||
@@ -324,29 +284,9 @@ export const DealTicket = ({
|
||||
}}
|
||||
render={() => (
|
||||
<TypeSelector
|
||||
value={
|
||||
order.type === OrderType.TYPE_LIMIT
|
||||
? DealTicketType.Limit
|
||||
: DealTicketType.Market
|
||||
}
|
||||
onValueChange={(dealTicketType) => {
|
||||
setDealTicketType(market.id, dealTicketType);
|
||||
if (
|
||||
dealTicketType !== DealTicketType.Limit &&
|
||||
dealTicketType !== DealTicketType.Market
|
||||
) {
|
||||
updateStopOrderFormValues(market.id, {
|
||||
type:
|
||||
dealTicketType === DealTicketType.StopLimit
|
||||
? OrderType.TYPE_LIMIT
|
||||
: OrderType.TYPE_MARKET,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const type =
|
||||
dealTicketType === DealTicketType.Limit
|
||||
? OrderType.TYPE_LIMIT
|
||||
: OrderType.TYPE_MARKET;
|
||||
value={order.type}
|
||||
onSelect={(type) => {
|
||||
if (type === OrderType.TYPE_NETWORK) return;
|
||||
update({
|
||||
type,
|
||||
// when changing type also update the TIF to what was last used of new type
|
||||
@@ -393,7 +333,7 @@ export const DealTicket = ({
|
||||
render={() => (
|
||||
<SideSelector
|
||||
value={order.side}
|
||||
onValueChange={(side) => {
|
||||
onSelect={(side) => {
|
||||
update({ side });
|
||||
}}
|
||||
/>
|
||||
@@ -404,7 +344,6 @@ export const DealTicket = ({
|
||||
orderType={order.type}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
marketPrice={marketPrice || undefined}
|
||||
sizeError={errors.size?.message}
|
||||
priceError={errors.price?.message}
|
||||
update={update}
|
||||
@@ -528,7 +467,9 @@ export const DealTicket = ({
|
||||
? t(
|
||||
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
||||
)
|
||||
: t(REDUCE_ONLY_TOOLTIP)}
|
||||
: t(
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -600,15 +541,9 @@ export const DealTicket = ({
|
||||
/>
|
||||
<DealTicketButton side={order.side} />
|
||||
<DealTicketFeeDetails
|
||||
order={
|
||||
normalizedOrder && { ...normalizedOrder, price: price || undefined }
|
||||
}
|
||||
notionalSize={notionalSize}
|
||||
assetSymbol={assetSymbol}
|
||||
market={market}
|
||||
/>
|
||||
<DealTicketMarginDetails
|
||||
onMarketClick={onMarketClick}
|
||||
feeEstimate={feeEstimate}
|
||||
notionalSize={notionalSize}
|
||||
assetSymbol={assetSymbol}
|
||||
marginAccountBalance={marginAccountBalance}
|
||||
generalAccountBalance={generalAccountBalance}
|
||||
@@ -634,55 +569,6 @@ interface SummaryMessageProps {
|
||||
onClickCollateral?: () => void;
|
||||
onDeposit: (assetId: string) => void;
|
||||
}
|
||||
|
||||
export const NoWalletWarning = ({
|
||||
isReadOnly,
|
||||
pubKey,
|
||||
asset,
|
||||
}: Pick<SummaryMessageProps, 'isReadOnly' | 'pubKey' | 'asset'>) => {
|
||||
const assetSymbol = asset.symbol;
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
{
|
||||
'You need to connect your own wallet to start trading on this market'
|
||||
}
|
||||
</InputError>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const SummaryMessage = memo(
|
||||
({
|
||||
errorMessage,
|
||||
@@ -697,16 +583,46 @@ const SummaryMessage = memo(
|
||||
}: SummaryMessageProps) => {
|
||||
// Specific error UI for if balance is so we can
|
||||
// render a deposit dialog
|
||||
if (isReadOnly || !pubKey) {
|
||||
const assetSymbol = asset.symbol;
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<NoWalletWarning
|
||||
isReadOnly={isReadOnly}
|
||||
asset={asset}
|
||||
pubKey={pubKey}
|
||||
/>
|
||||
<div className="mb-2">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
{
|
||||
'You need to connect your own wallet to start trading on this market'
|
||||
}
|
||||
</InputError>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user