Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3506fce0b6 | ||
|
|
3bfd13d261 | ||
|
|
3bbbcd9be1 | ||
|
|
042e5fd23b | ||
|
|
13a47f6439 | ||
|
|
b1ea86b081 | ||
|
|
b943adfe13 | ||
|
|
16f88e9577 | ||
|
|
2944e578d8 | ||
|
|
51ab1005b9 | ||
|
|
f6965ce119 | ||
|
|
9bffbbd557 | ||
|
|
ea3af6a234 |
@@ -19,7 +19,7 @@ jobs:
|
|||||||
create-docker-image:
|
create-docker-image:
|
||||||
name: Create docker image for console-test
|
name: Create docker image for console-test
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 45
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
# check-out frontend-monorepo
|
# check-out frontend-monorepo
|
||||||
@@ -138,7 +138,7 @@ jobs:
|
|||||||
name: run-tests
|
name: run-tests
|
||||||
runs-on: 8-cores
|
runs-on: 8-cores
|
||||||
needs: [create-docker-image, console-test-branch]
|
needs: [create-docker-image, console-test-branch]
|
||||||
timeout-minutes: 45
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
# load docker image
|
# load docker image
|
||||||
@@ -205,7 +205,7 @@ jobs:
|
|||||||
# run tests
|
# run tests
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 1 --dist loadfile --durations=45
|
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 4 --dist loadfile --durations=15
|
||||||
working-directory: apps/trading/e2e
|
working-directory: apps/trading/e2e
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
# upload traces
|
# upload traces
|
||||||
@@ -227,46 +227,3 @@ jobs:
|
|||||||
name: worker-logs
|
name: worker-logs
|
||||||
path: ./logs/
|
path: ./logs/
|
||||||
retention-days: 15
|
retention-days: 15
|
||||||
#----------------------------------------------
|
|
||||||
# ----- upload market-sim logs -----
|
|
||||||
#----------------------------------------------
|
|
||||||
- name: Prepare and Zip market-sim-logs
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
parent_dir="/tmp/market-sim-logs"
|
|
||||||
echo "Creating parent directory at $parent_dir"
|
|
||||||
mkdir -p "$parent_dir"
|
|
||||||
|
|
||||||
echo "Waiting for vega-sim-* folders to be created..."
|
|
||||||
sleep 10 # Waits 10 seconds to ensure all folders are created
|
|
||||||
|
|
||||||
echo "Before searching for vega-sim-* folders in /tmp..."
|
|
||||||
folders=$(find /tmp -mindepth 1 -type d -name 'vega-sim-*' -print) || echo "Find command failed with exit code $?"
|
|
||||||
|
|
||||||
echo "After searching for vega-sim-* folders in /tmp..."
|
|
||||||
|
|
||||||
if [ -z "$folders" ]; then
|
|
||||||
echo "No vega-sim-* folders found."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Moving vega-sim-* folders to $parent_dir"
|
|
||||||
echo "$folders" | xargs -I {} mv {} "$parent_dir/"
|
|
||||||
|
|
||||||
echo "Checking if $parent_dir is not empty..."
|
|
||||||
if [ "$(ls -A $parent_dir)" ]; then
|
|
||||||
echo "Zipping the parent directory..."
|
|
||||||
zip -r market-sim-logs.zip "$parent_dir" && echo "Zip file created successfully."
|
|
||||||
else
|
|
||||||
echo "$parent_dir is empty. No zip file created."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
shell: /usr/bin/bash -e {0}
|
|
||||||
|
|
||||||
- name: Upload market-sim-logs
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: market-sim-logs
|
|
||||||
path: market-sim-logs.zip
|
|
||||||
retention-days: 15
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
|
|||||||
cy.getByTestId('icon-cross').click();
|
cy.getByTestId('icon-cross').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Proposal page displayed on mobile', function () {
|
it.skip('Proposal page displayed on mobile', function () {
|
||||||
const proposalTitle = 'Add Lorem Ipsum market';
|
const proposalTitle = 'Add Lorem Ipsum market';
|
||||||
|
|
||||||
cy.common_switch_to_mobile_and_click_toggle();
|
cy.common_switch_to_mobile_and_click_toggle();
|
||||||
@@ -55,7 +55,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Able to view new asset proposal', function () {
|
it('Able to view new asset proposal', function () {
|
||||||
const proposalTitle = 'Test new asset proposal';
|
const proposalTitle = 'Test new asset proposal';
|
||||||
const newAssetProposalBody = getNewAssetTxBody();
|
const newAssetProposalBody = getNewAssetTxBody();
|
||||||
cy.VegaWalletSubmitProposal(newAssetProposalBody);
|
cy.VegaWalletSubmitProposal(newAssetProposalBody);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className="whitespace-nowrap">
|
||||||
{useName && <Icon size={4} name="cube" className="mr-2" />}
|
{useName && <Icon size={4} name="cube" className="mr-2" />}
|
||||||
<Link
|
<Link
|
||||||
className="underline font-mono"
|
className="underline font-mono"
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
import { NestedDataList, sortNestedDataByChildren } from './nested-data-list';
|
import {
|
||||||
|
BORDER_COLOURS,
|
||||||
|
NestedDataList,
|
||||||
|
sortNestedDataByChildren,
|
||||||
|
} from './nested-data-list';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
@@ -57,6 +61,38 @@ describe('NestedDataList', () => {
|
|||||||
expect(parent[0].querySelector('li')).toHaveClass('pl-4 border-l-4 pt-2');
|
expect(parent[0].querySelector('li')).toHaveClass('pl-4 border-l-4 pt-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should repeat the border colours in the correct order', () => {
|
||||||
|
const colourMockData = {
|
||||||
|
t0: {
|
||||||
|
t1: {
|
||||||
|
t2: {
|
||||||
|
t3: {
|
||||||
|
t4: {
|
||||||
|
t5: {
|
||||||
|
t6: {
|
||||||
|
t7: {
|
||||||
|
t8: {
|
||||||
|
hello: 'world',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const tree = render(<NestedDataList data={colourMockData} />);
|
||||||
|
const { getByTestId } = tree;
|
||||||
|
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const item = getByTestId(`T${i}`);
|
||||||
|
const expected = BORDER_COLOURS.light[i % 5];
|
||||||
|
expect(item.style.borderColor.toUpperCase()).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should sort the data by values with children', () => {
|
it('should sort the data by values with children', () => {
|
||||||
const mockData = {
|
const mockData = {
|
||||||
nonce: '5980890939790185837',
|
nonce: '5980890939790185837',
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const DeterministicOrderDetails = ({
|
|||||||
const o = data.orderByID;
|
const o = data.orderByID;
|
||||||
return (
|
return (
|
||||||
<div className={wrapperClasses}>
|
<div className={wrapperClasses}>
|
||||||
<div className="mb-0">
|
<div className="mb-12 lg:mb-0">
|
||||||
<div className="relative block px-3 py-6 md:px-6 lg:-mr-7">
|
<div className="relative block px-3 py-6 md:px-6 lg:-mr-7">
|
||||||
<h2 className="text-3xl font-bold mb-4 display-5">
|
<h2 className="text-3xl font-bold mb-4 display-5">
|
||||||
<abbr title={tifFull[o.timeInForce]} className="bb-dotted mr-2">
|
<abbr title={tifFull[o.timeInForce]} className="bb-dotted mr-2">
|
||||||
@@ -89,9 +89,9 @@ const DeterministicOrderDetails = ({
|
|||||||
<span>{t('Reference')}</span>: {o.reference}
|
<span>{t('Reference')}</span>: {o.reference}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-x-6 mt-4">
|
<div className="grid md:grid-cols-5 gap-x-6 mt-4">
|
||||||
<div className="mb-6 md:mb-0">
|
<div className="mb-12 md:mb-0">
|
||||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||||
{t('Status')}
|
{t('Status')}
|
||||||
</h2>
|
</h2>
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
|
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
|
||||||
@@ -99,17 +99,15 @@ const DeterministicOrderDetails = ({
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6 md:mb-0">
|
<div className="mb-12 md:mb-0">
|
||||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
|
||||||
{t('Size')}
|
|
||||||
</h2>
|
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||||
<SizeInMarket size={o.size} marketId={o.market.id} />
|
<SizeInMarket size={o.size} marketId={o.market.id} />
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6 md:mb-0">
|
<div className="">
|
||||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||||
{t('Version')}
|
{t('Version')}
|
||||||
</h2>
|
</h2>
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||||
@@ -117,8 +115,8 @@ const DeterministicOrderDetails = ({
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
{o.type ? (
|
{o.type ? (
|
||||||
<div className="mb-6 md:mb-0">
|
<div className="">
|
||||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||||
{t('Type')}
|
{t('Type')}
|
||||||
</h2>
|
</h2>
|
||||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ export const Signature = ({ signature }: SignatureProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex border rounded signature-component relative pr-[20px]">
|
<div className="inline-flex border rounded signature-component relative pr-[20px]">
|
||||||
<div
|
<span
|
||||||
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
|
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
|
||||||
title={`${signature.algo}`}
|
title={`Version ${signature.version}`}
|
||||||
>
|
>
|
||||||
<span>v{signature.version}</span>
|
{signature.algo}
|
||||||
</div>
|
</span>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isOpen
|
isOpen
|
||||||
|
|||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
import * as Types from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
|
||||||
const defaultOptions = {} as const;
|
|
||||||
export type ExplorerReferralCodeOwnerQueryVariables = Types.Exact<{
|
|
||||||
id: Types.Scalars['ID'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerReferralCodeOwnerQuery = { __typename?: 'Query', referralSets: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', createdAt: any, updatedAt: any, referrer: string } } | null> } };
|
|
||||||
|
|
||||||
|
|
||||||
export const ExplorerReferralCodeOwnerDocument = gql`
|
|
||||||
query ExplorerReferralCodeOwner($id: ID!) {
|
|
||||||
referralSets(id: $id) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
referrer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useExplorerReferralCodeOwnerQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useExplorerReferralCodeOwnerQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useExplorerReferralCodeOwnerQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useExplorerReferralCodeOwnerQuery({
|
|
||||||
* variables: {
|
|
||||||
* id: // value for 'id'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useExplorerReferralCodeOwnerQuery(baseOptions: Apollo.QueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
|
|
||||||
}
|
|
||||||
export function useExplorerReferralCodeOwnerLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
|
|
||||||
}
|
|
||||||
export type ExplorerReferralCodeOwnerQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerQuery>;
|
|
||||||
export type ExplorerReferralCodeOwnerLazyQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerLazyQuery>;
|
|
||||||
export type ExplorerReferralCodeOwnerQueryResult = Apollo.QueryResult<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
query ExplorerReferralCodeOwner($id: ID!) {
|
|
||||||
referralSets(id: $id) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
referrer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { render, waitFor } from '@testing-library/react';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { ReferralCodeOwner } from './referral-code-owner';
|
|
||||||
import type { ReferralCodeOwnerProps } from './referral-code-owner';
|
|
||||||
import { ExplorerReferralCodeOwnerDocument } from './__generated__/code-owner';
|
|
||||||
const renderComponent = (
|
|
||||||
props: ReferralCodeOwnerProps,
|
|
||||||
mocks: MockedResponse[]
|
|
||||||
) => {
|
|
||||||
return render(
|
|
||||||
<MockedProvider mocks={mocks}>
|
|
||||||
<MemoryRouter>
|
|
||||||
<ReferralCodeOwner {...props} />
|
|
||||||
</MemoryRouter>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('ReferralCodeOwner', () => {
|
|
||||||
it('should render loading state', () => {
|
|
||||||
const mocks = [
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
query: ExplorerReferralCodeOwnerDocument,
|
|
||||||
variables: {
|
|
||||||
id: 'ABC123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
|
|
||||||
|
|
||||||
expect(getByText('Loading...')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render error state', async () => {
|
|
||||||
const errorMessage = 'Error fetching referrer: ABC123';
|
|
||||||
const mocks = [
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
query: ExplorerReferralCodeOwnerDocument,
|
|
||||||
variables: {
|
|
||||||
id: 'ABC123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error: new Error('nope'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(getByText(errorMessage)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render link to referring party', async () => {
|
|
||||||
const referrerId = 'DEF789';
|
|
||||||
|
|
||||||
const mocks = [
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
query: ExplorerReferralCodeOwnerDocument,
|
|
||||||
variables: {
|
|
||||||
id: 'ABC123',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
referralSets: {
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
__typename: 'ReferralSet',
|
|
||||||
referrer: referrerId,
|
|
||||||
createdAt: '123',
|
|
||||||
updatedAt: '456',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(getByText(referrerId)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { TableCell } from '../../../table';
|
|
||||||
import { useExplorerReferralCodeOwnerQuery } from './__generated__/code-owner';
|
|
||||||
import { PartyLink } from '../../../links';
|
|
||||||
|
|
||||||
export interface ReferralCodeOwnerProps {
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the owner of a referral code
|
|
||||||
*/
|
|
||||||
export const ReferralCodeOwner = ({ code }: ReferralCodeOwnerProps) => {
|
|
||||||
const { data, error, loading } = useExplorerReferralCodeOwnerQuery({
|
|
||||||
variables: {
|
|
||||||
id: code,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const referrer = data?.referralSets.edges[0]?.node.referrer || '';
|
|
||||||
return (
|
|
||||||
<TableCell>
|
|
||||||
{loading && 'Loading...'}
|
|
||||||
{error && `Error fetching referrer: ${code}`}
|
|
||||||
{referrer.length > 0 && <PartyLink id={referrer} />}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { render } from '@testing-library/react';
|
|
||||||
import { ReferralTeam } from './team';
|
|
||||||
import type { CreateReferralSet } from './team';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
|
|
||||||
describe('ReferralTeam', () => {
|
|
||||||
const team = {
|
|
||||||
name: 'Test Team',
|
|
||||||
teamUrl: 'https://example.com/team',
|
|
||||||
avatarUrl: 'https://example.com/avatar',
|
|
||||||
closed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockTx: CreateReferralSet = {
|
|
||||||
team,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockId = '123456';
|
|
||||||
const mockCreator = 'JohnDoe';
|
|
||||||
|
|
||||||
it('should render the team name', () => {
|
|
||||||
const { getByText } = render(
|
|
||||||
<MockedProvider>
|
|
||||||
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
expect(getByText('Test Team')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the team ID', () => {
|
|
||||||
const { getByText } = render(
|
|
||||||
<MockedProvider>
|
|
||||||
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
expect(getByText('Id')).toBeInTheDocument();
|
|
||||||
expect(getByText(mockId)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the creator', () => {
|
|
||||||
const { getByText } = render(
|
|
||||||
<MockedProvider>
|
|
||||||
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
expect(getByText('Creator')).toBeInTheDocument();
|
|
||||||
expect(getByText(mockCreator)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the team URL', () => {
|
|
||||||
const { getByText } = render(
|
|
||||||
<MockedProvider>
|
|
||||||
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
expect(getByText('Team URL')).toBeInTheDocument();
|
|
||||||
expect(getByText(team.teamUrl)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the avatar URL', () => {
|
|
||||||
const { getByText } = render(
|
|
||||||
<MockedProvider>
|
|
||||||
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
expect(getByText('Avatar')).toBeInTheDocument();
|
|
||||||
expect(getByText(team.avatarUrl)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the open status as a tick if closed is falsy', () => {
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<MockedProvider>
|
|
||||||
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
expect(getByTestId('open-yes')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the open status as a cross if it is truthy', () => {
|
|
||||||
const m = {
|
|
||||||
team: {
|
|
||||||
closed: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<MockedProvider>
|
|
||||||
<ReferralTeam tx={m} id={mockId} creator={mockCreator} />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
expect(getByTestId('open-no')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import {
|
|
||||||
VegaIcon,
|
|
||||||
Icon,
|
|
||||||
KeyValueTable,
|
|
||||||
KeyValueTableRow,
|
|
||||||
VegaIconNames,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { components } from '../../../../../types/explorer';
|
|
||||||
import Hash from '../../../links/hash';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { PartyLink } from '../../../links';
|
|
||||||
|
|
||||||
export type CreateReferralSet = components['schemas']['v1CreateReferralSet'];
|
|
||||||
export type ReferralTeam = CreateReferralSet['team'];
|
|
||||||
|
|
||||||
export interface ReferralTeamProps {
|
|
||||||
tx: CreateReferralSet;
|
|
||||||
id: string;
|
|
||||||
creator: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the details for a team in a CreateReferralSet or UpdateReferralSet transaction.
|
|
||||||
*
|
|
||||||
* Intentionally does not render the avatar image or link to the team url.
|
|
||||||
*/
|
|
||||||
export const ReferralTeam = ({ tx, id, creator }: ReferralTeamProps) => {
|
|
||||||
if (!tx.team) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<div className="inline-block mr-2 leading-none">
|
|
||||||
<VegaIcon name={VegaIconNames.TEAM} />
|
|
||||||
</div>
|
|
||||||
{tx.team.name && (
|
|
||||||
<h3 className="inline-block leading-loose">{tx.team.name}</h3>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="min-w-fit max-w-2xl block">
|
|
||||||
<KeyValueTable>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('Id')}
|
|
||||||
<Hash text={id} truncate={false} />
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('Creator')}
|
|
||||||
<PartyLink id={creator} truncate={false} />
|
|
||||||
</KeyValueTableRow>
|
|
||||||
{tx.team.teamUrl && (
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('Team URL')}
|
|
||||||
<Hash text={tx.team.teamUrl} truncate={false} />
|
|
||||||
</KeyValueTableRow>
|
|
||||||
)}
|
|
||||||
{tx.team.avatarUrl && (
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('Avatar')}
|
|
||||||
<Hash text={tx.team.avatarUrl} truncate={false} />
|
|
||||||
</KeyValueTableRow>
|
|
||||||
)}
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('Open')}
|
|
||||||
<span data-testid={!tx.team.closed ? 'open-yes' : 'open-no'}>
|
|
||||||
{!tx.team.closed ? <Icon name="tick" /> : <Icon name="cross" />}
|
|
||||||
</span>
|
|
||||||
</KeyValueTableRow>
|
|
||||||
</KeyValueTable>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -27,8 +27,7 @@ export const sharedHeaderProps = {
|
|||||||
className: 'align-top',
|
className: 'align-top',
|
||||||
};
|
};
|
||||||
|
|
||||||
// The incoming type field is usually the right thing to show. Exceptions are listed here
|
const Labels: Record<BlockExplorerTransactionResult['type'], string> = {
|
||||||
const LabelOverrides: Record<BlockExplorerTransactionResult['type'], string> = {
|
|
||||||
'Stop Orders Submission': 'Stop Order',
|
'Stop Orders Submission': 'Stop Order',
|
||||||
'Stop Orders Cancellation': 'Cancel Stop Order',
|
'Stop Orders Cancellation': 'Cancel Stop Order',
|
||||||
};
|
};
|
||||||
@@ -51,7 +50,7 @@ export const TxDetailsShared = ({
|
|||||||
const time: string = blockData?.result.block.header.time || '';
|
const time: string = blockData?.result.block.header.time || '';
|
||||||
const height: string = blockData?.result.block.header.height || txData.block;
|
const height: string = blockData?.result.block.header.height || txData.block;
|
||||||
|
|
||||||
const type = LabelOverrides[txData.type] || txData.type;
|
const type = Labels[txData.type] || txData.type;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
|
||||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
|
||||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
|
||||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
|
||||||
import { ReferralCodeOwner } from './referrals/referral-code-owner';
|
|
||||||
|
|
||||||
interface TxDetailsApplyReferralCodeProps {
|
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
|
||||||
pubKey: string | undefined;
|
|
||||||
blockData: TendermintBlocksResponse | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The signature can be turned in to an id with txSignatureToDeterministicId but
|
|
||||||
*/
|
|
||||||
export const TxDetailsApplyReferralCode = ({
|
|
||||||
txData,
|
|
||||||
pubKey,
|
|
||||||
blockData,
|
|
||||||
}: TxDetailsApplyReferralCodeProps) => {
|
|
||||||
if (!txData || !txData.command.applyReferralCode || !txData.signature) {
|
|
||||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const referralCode = txData.command.applyReferralCode.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
|
||||||
<TxDetailsShared
|
|
||||||
txData={txData}
|
|
||||||
pubKey={pubKey}
|
|
||||||
blockData={blockData}
|
|
||||||
/>
|
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{t('Applied Code')}</TableCell>
|
|
||||||
<TableCell>{referralCode}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{t('Referrer')}</TableCell>
|
|
||||||
<ReferralCodeOwner code={referralCode} />
|
|
||||||
</TableRow>
|
|
||||||
</TableWithTbody>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
|
||||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
|
||||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
|
||||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
|
||||||
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
|
||||||
import { ReferralTeam } from './referrals/team';
|
|
||||||
|
|
||||||
interface TxDetailsCreateReferralProps {
|
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
|
||||||
pubKey: string | undefined;
|
|
||||||
blockData: TendermintBlocksResponse | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The signature can be turned in to an id with txSignatureToDeterministicId but
|
|
||||||
*/
|
|
||||||
export const TxDetailsCreateReferralSet = ({
|
|
||||||
txData,
|
|
||||||
pubKey,
|
|
||||||
blockData,
|
|
||||||
}: TxDetailsCreateReferralProps) => {
|
|
||||||
if (!txData || !txData.command.createReferralSet || !txData.signature) {
|
|
||||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = txSignatureToDeterministicId(txData.signature.value);
|
|
||||||
|
|
||||||
const isTeam = txData.command.createReferralSet.isTeam || false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
|
||||||
<TxDetailsShared
|
|
||||||
txData={txData}
|
|
||||||
pubKey={pubKey}
|
|
||||||
blockData={blockData}
|
|
||||||
/>
|
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{isTeam ? t('Team ID') : t('Referral code')}</TableCell>
|
|
||||||
<TableCell>{id}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableWithTbody>
|
|
||||||
|
|
||||||
<ReferralTeam
|
|
||||||
tx={txData.command.createReferralSet}
|
|
||||||
id={id}
|
|
||||||
creator={txData.submitter}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -28,10 +28,6 @@ import { TxProposal } from './tx-proposal';
|
|||||||
import { TxDetailsTransfer } from './tx-transfer';
|
import { TxDetailsTransfer } from './tx-transfer';
|
||||||
import { TxDetailsStopOrderSubmission } from './tx-stop-order-submission';
|
import { TxDetailsStopOrderSubmission } from './tx-stop-order-submission';
|
||||||
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
||||||
import { TxDetailsCreateReferralSet } from './tx-create-referral-set';
|
|
||||||
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
|
|
||||||
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
|
||||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
|
||||||
|
|
||||||
interface TxDetailsWrapperProps {
|
interface TxDetailsWrapperProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@@ -125,14 +121,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
|||||||
return TxDetailsStopOrderSubmission;
|
return TxDetailsStopOrderSubmission;
|
||||||
case 'Transfer Funds':
|
case 'Transfer Funds':
|
||||||
return TxDetailsTransfer;
|
return TxDetailsTransfer;
|
||||||
case 'Create Referral Set':
|
|
||||||
return TxDetailsCreateReferralSet;
|
|
||||||
case 'Update Referral Set':
|
|
||||||
return TxDetailsUpdateReferralSet;
|
|
||||||
case 'Apply Referral Code':
|
|
||||||
return TxDetailsApplyReferralCode;
|
|
||||||
case 'Join Team':
|
|
||||||
return TxDetailsJoinTeam;
|
|
||||||
default:
|
default:
|
||||||
return TxDetailsGeneric;
|
return TxDetailsGeneric;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
|
||||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
|
||||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
|
||||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
|
||||||
import { ReferralCodeOwner } from './referrals/referral-code-owner';
|
|
||||||
|
|
||||||
interface TxDetailsJoinTeamProps {
|
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
|
||||||
pubKey: string | undefined;
|
|
||||||
blockData: TendermintBlocksResponse | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The signature can be turned in to an id with txSignatureToDeterministicId but
|
|
||||||
*/
|
|
||||||
export const TxDetailsJoinTeam = ({
|
|
||||||
txData,
|
|
||||||
pubKey,
|
|
||||||
blockData,
|
|
||||||
}: TxDetailsJoinTeamProps) => {
|
|
||||||
if (!txData || !txData.command.joinTeam || !txData.signature) {
|
|
||||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const team = txData.command.joinTeam.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
|
||||||
<TxDetailsShared
|
|
||||||
txData={txData}
|
|
||||||
pubKey={pubKey}
|
|
||||||
blockData={blockData}
|
|
||||||
/>
|
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{t('Team')}</TableCell>
|
|
||||||
<TableCell>{team}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{t('Referrer')}</TableCell>
|
|
||||||
<ReferralCodeOwner code={team} />
|
|
||||||
</TableRow>
|
|
||||||
</TableWithTbody>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
|
||||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
|
||||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
|
||||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
|
||||||
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
|
||||||
import { ReferralTeam } from './referrals/team';
|
|
||||||
|
|
||||||
interface TxDetailsUpdateReferralProps {
|
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
|
||||||
pubKey: string | undefined;
|
|
||||||
blockData: TendermintBlocksResponse | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A copy of create referral set, effectively.
|
|
||||||
* Updating a referral set without a team doesn't make sense,
|
|
||||||
* but is valid, so this component ignores sense.
|
|
||||||
*/
|
|
||||||
export const TxDetailsUpdateReferralSet = ({
|
|
||||||
txData,
|
|
||||||
pubKey,
|
|
||||||
blockData,
|
|
||||||
}: TxDetailsUpdateReferralProps) => {
|
|
||||||
if (!txData || !txData.command.updateReferralSet || !txData.signature) {
|
|
||||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = txSignatureToDeterministicId(txData.signature.value);
|
|
||||||
|
|
||||||
const isTeam = txData.command.updateReferralSet.isTeam || false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
|
||||||
<TxDetailsShared
|
|
||||||
txData={txData}
|
|
||||||
pubKey={pubKey}
|
|
||||||
blockData={blockData}
|
|
||||||
/>
|
|
||||||
<TableRow modifier="bordered">
|
|
||||||
<TableCell>{isTeam ? t('Team ID') : t('Referral code')}</TableCell>
|
|
||||||
<TableCell>{id}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableWithTbody>
|
|
||||||
|
|
||||||
<ReferralTeam
|
|
||||||
tx={txData.command.updateReferralSet}
|
|
||||||
id={id}
|
|
||||||
creator={txData.submitter}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -28,7 +28,6 @@ export type FilterOption =
|
|||||||
| 'Delegate'
|
| 'Delegate'
|
||||||
| 'Ethereum Key Rotate Submission'
|
| 'Ethereum Key Rotate Submission'
|
||||||
| 'Issue Signatures'
|
| 'Issue Signatures'
|
||||||
| 'Join Team'
|
|
||||||
| 'Key Rotate Submission'
|
| 'Key Rotate Submission'
|
||||||
| 'Liquidity Provision Order'
|
| 'Liquidity Provision Order'
|
||||||
| 'Node Signature'
|
| 'Node Signature'
|
||||||
@@ -48,46 +47,45 @@ export type FilterOption =
|
|||||||
| 'Vote on Proposal'
|
| 'Vote on Proposal'
|
||||||
| 'Withdraw';
|
| 'Withdraw';
|
||||||
|
|
||||||
export const filterOptions: Record<string, FilterOption[]> = {
|
// Alphabetised list of transaction types to appear at the top level
|
||||||
'Market Instructions': [
|
export const PrimaryFilterOptions: FilterOption[] = [
|
||||||
'Amend LiquidityProvision Order',
|
'Amend LiquidityProvision Order',
|
||||||
'Amend Order',
|
'Amend Order',
|
||||||
'Batch Market Instructions',
|
'Batch Market Instructions',
|
||||||
'Cancel LiquidityProvision Order',
|
'Cancel LiquidityProvision Order',
|
||||||
'Cancel Order',
|
'Cancel Order',
|
||||||
'Liquidity Provision Order',
|
'Cancel Transfer Funds',
|
||||||
'Stop Orders Submission',
|
'Delegate',
|
||||||
'Stop Orders Cancellation',
|
'Liquidity Provision Order',
|
||||||
'Submit Order',
|
'Proposal',
|
||||||
],
|
'Stop Orders Submission',
|
||||||
'Transfers and Withdrawals': [
|
'Stop Orders Cancellation',
|
||||||
'Transfer Funds',
|
'Submit Oracle Data',
|
||||||
'Cancel Transfer Funds',
|
'Submit Order',
|
||||||
'Withdraw',
|
'Transfer Funds',
|
||||||
],
|
'Undelegate',
|
||||||
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
|
'Vote on Proposal',
|
||||||
Referrals: [
|
'Withdraw',
|
||||||
'Apply Referral Code',
|
];
|
||||||
'Create Referral Set',
|
|
||||||
'Join Team',
|
|
||||||
'Update Referral Set',
|
|
||||||
],
|
|
||||||
'External Data': ['Chain Event', 'Submit Oracle Data'],
|
|
||||||
Validators: [
|
|
||||||
'Ethereum Key Rotate Submission',
|
|
||||||
'Issue Signatures',
|
|
||||||
'Key Rotate Submission',
|
|
||||||
'Node Signature',
|
|
||||||
'Node Vote',
|
|
||||||
'Protocol Upgrade',
|
|
||||||
'Register new Node',
|
|
||||||
'State Variable Proposal',
|
|
||||||
'Validator Heartbeat',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllFilterOptions: FilterOption[] =
|
// Alphabetised list of transaction types to nest under a 'More...' submenu
|
||||||
Object.values(filterOptions).flat();
|
export const SecondaryFilterOptions: FilterOption[] = [
|
||||||
|
'Chain Event',
|
||||||
|
'Ethereum Key Rotate Submission',
|
||||||
|
'Issue Signatures',
|
||||||
|
'Key Rotate Submission',
|
||||||
|
'Node Signature',
|
||||||
|
'Node Vote',
|
||||||
|
'Protocol Upgrade',
|
||||||
|
'Register new Node',
|
||||||
|
'State Variable Proposal',
|
||||||
|
'Validator Heartbeat',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AllFilterOptions: FilterOption[] = [
|
||||||
|
...PrimaryFilterOptions,
|
||||||
|
...SecondaryFilterOptions,
|
||||||
|
];
|
||||||
|
|
||||||
export interface TxFilterProps {
|
export interface TxFilterProps {
|
||||||
filters: Set<FilterOption>;
|
filters: Set<FilterOption>;
|
||||||
@@ -124,33 +122,46 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{PrimaryFilterOptions.map((f) => (
|
||||||
{Object.entries(filterOptions).map(([key, value]) => (
|
<DropdownMenuCheckboxItem
|
||||||
<DropdownMenuSub key={key}>
|
key={f}
|
||||||
<DropdownMenuSubTrigger>
|
checked={filters.has(f)}
|
||||||
{t(key)}
|
onCheckedChange={() => {
|
||||||
<Icon name="chevron-right" />
|
// NOTE: These act like radio buttons until the API supports multiple filters
|
||||||
</DropdownMenuSubTrigger>
|
setFilters(new Set([f]));
|
||||||
<DropdownMenuSubContent>
|
}}
|
||||||
{value.map((f) => (
|
id={`radio-${f}`}
|
||||||
<DropdownMenuCheckboxItem
|
>
|
||||||
key={f}
|
{f}
|
||||||
checked={filters.has(f)}
|
<DropdownMenuItemIndicator>
|
||||||
onCheckedChange={(checked) => {
|
<Icon name="tick-circle" />
|
||||||
// NOTE: These act like radio buttons until the API supports multiple filters
|
</DropdownMenuItemIndicator>
|
||||||
setFilters(new Set([f]));
|
</DropdownMenuCheckboxItem>
|
||||||
}}
|
|
||||||
id={`radio-${f}`}
|
|
||||||
>
|
|
||||||
{f}
|
|
||||||
<DropdownMenuItemIndicator>
|
|
||||||
<Icon name="tick-circle" className="inline" />
|
|
||||||
</DropdownMenuItemIndicator>
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuSubContent>
|
|
||||||
</DropdownMenuSub>
|
|
||||||
))}
|
))}
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
{t('More Types')}
|
||||||
|
<Icon name="chevron-right" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
{SecondaryFilterOptions.map((f) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={f}
|
||||||
|
checked={filters.has(f)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
// NOTE: These act like radio buttons until the API supports multiple filters
|
||||||
|
setFilters(new Set([f]));
|
||||||
|
}}
|
||||||
|
id={`radio-${f}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon name="tick-circle" className="inline" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ const displayString: StringMap = {
|
|||||||
StopOrdersSubmission: 'Stop',
|
StopOrdersSubmission: 'Stop',
|
||||||
StopOrdersCancellation: 'Cancel stop',
|
StopOrdersCancellation: 'Cancel stop',
|
||||||
'Stop Orders Cancellation': 'Cancel stop',
|
'Stop Orders Cancellation': 'Cancel stop',
|
||||||
'Apply Referral Code': 'Referral',
|
|
||||||
'Create Referral Set': 'Create referral',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getLabelForStopOrderType(
|
export function getLabelForStopOrderType(
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const Layout = () => {
|
|||||||
<ProtocolUpgradeInProgressNotification />
|
<ProtocolUpgradeInProgressNotification />
|
||||||
</div>
|
</div>
|
||||||
<div className={fixedWidthClasses}>
|
<div className={fixedWidthClasses}>
|
||||||
<main className="md:p-4">
|
<main className="p-4">
|
||||||
{!isHome && <BreadcrumbsContainer className="mb-4" />}
|
{!isHome && <BreadcrumbsContainer className="mb-4" />}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ const Tx = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !data?.transaction) {
|
|
||||||
errorMessage = 'Transaction not found';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -53,7 +49,7 @@ const Tx = () => {
|
|||||||
<TxDetails
|
<TxDetails
|
||||||
className="mb-28"
|
className="mb-28"
|
||||||
txData={data?.transaction}
|
txData={data?.transaction}
|
||||||
pubKey={data?.transaction?.submitter}
|
pubKey={data?.transaction.submitter}
|
||||||
/>
|
/>
|
||||||
</RenderFetched>
|
</RenderFetched>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ interface TxDetailsProps {
|
|||||||
export const txDetailsTruncateLength = 30;
|
export const txDetailsTruncateLength = 30;
|
||||||
|
|
||||||
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
|
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
|
||||||
if (!txData || !pubKey) {
|
if (!txData) {
|
||||||
return <>{t('Transaction could not be found')}</>;
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className="mb-10" key={txData.hash}>
|
<section className="mb-10" key={txData.hash}>
|
||||||
|
|||||||
+2
-2
@@ -8,13 +8,13 @@ type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
|||||||
type XOR<T, U> = T | U extends object
|
type XOR<T, U> = T | U extends object
|
||||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||||
: T | U;
|
: T | U;
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
type OneOf<T extends any[]> = T extends [infer Only]
|
type OneOf<T extends any[]> = T extends [infer Only]
|
||||||
? Only
|
? Only
|
||||||
: T extends [infer A, infer B, ...infer Rest]
|
: T extends [infer A, infer B, ...infer Rest]
|
||||||
? OneOf<[XOR<A, B>, ...Rest]>
|
? OneOf<[XOR<A, B>, ...Rest]>
|
||||||
: never;
|
: never;
|
||||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
export interface paths {
|
export interface paths {
|
||||||
'/info': {
|
'/info': {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
|
|||||||
.and('contain.text', 'USDC (fake)');
|
.and('contain.text', 'USDC (fake)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Unable to submit proposal with public key', function () {
|
it('Unable to submit proposal with public key', function () {
|
||||||
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
|
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
|
||||||
|
|
||||||
goToMakeNewProposal(governanceProposalType.RAW);
|
goToMakeNewProposal(governanceProposalType.RAW);
|
||||||
@@ -55,10 +55,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
|
|||||||
cy.getByTestId('dialog-content')
|
cy.getByTestId('dialog-content')
|
||||||
.first()
|
.first()
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.getByTestId('dialog-title').should(
|
cy.get('h1').should('have.text', 'Transaction failed');
|
||||||
'have.text',
|
|
||||||
'Transaction failed'
|
|
||||||
);
|
|
||||||
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
|
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ export function createNewMarketProposalTxBody(): ProposalSubmissionBody {
|
|||||||
performanceHysteresisEpochs: 2,
|
performanceHysteresisEpochs: 2,
|
||||||
slaCompetitionFactor: '0.1',
|
slaCompetitionFactor: '0.1',
|
||||||
},
|
},
|
||||||
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
|
|
||||||
quadraticSlippageFactor: '0',
|
quadraticSlippageFactor: '0',
|
||||||
instrument: {
|
instrument: {
|
||||||
name: 'Token test market',
|
name: 'Token test market',
|
||||||
@@ -197,7 +196,6 @@ export function createNewMarketProposalTxBody(): ProposalSubmissionBody {
|
|||||||
timeWindow: '3600',
|
timeWindow: '3600',
|
||||||
scalingFactor: 10,
|
scalingFactor: 10,
|
||||||
},
|
},
|
||||||
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
|
|
||||||
triggeringRatio: '0.7',
|
triggeringRatio: '0.7',
|
||||||
auctionExtension: '1',
|
auctionExtension: '1',
|
||||||
},
|
},
|
||||||
@@ -242,7 +240,6 @@ export function createSuccessorMarketProposalTxBody(
|
|||||||
decimalPlaces: '5',
|
decimalPlaces: '5',
|
||||||
positionDecimalPlaces: '5',
|
positionDecimalPlaces: '5',
|
||||||
linearSlippageFactor: '0.001',
|
linearSlippageFactor: '0.001',
|
||||||
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
|
|
||||||
quadraticSlippageFactor: '0',
|
quadraticSlippageFactor: '0',
|
||||||
liquiditySlaParameters: {
|
liquiditySlaParameters: {
|
||||||
priceRange: '0.5',
|
priceRange: '0.5',
|
||||||
@@ -337,7 +334,6 @@ export function createSuccessorMarketProposalTxBody(
|
|||||||
timeWindow: '3600',
|
timeWindow: '3600',
|
||||||
scalingFactor: 10,
|
scalingFactor: 10,
|
||||||
},
|
},
|
||||||
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
|
|
||||||
triggeringRatio: '0.7',
|
triggeringRatio: '0.7',
|
||||||
auctionExtension: '1',
|
auctionExtension: '1',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
import { Web3Provider } from '@vegaprotocol/web3';
|
import { Web3Provider } from '@vegaprotocol/web3';
|
||||||
import { VegaWalletDialogs } from './components/vega-wallet-dialogs';
|
import { VegaWalletDialogs } from './components/vega-wallet-dialogs';
|
||||||
import { VegaWalletProvider, useChainId } from '@vegaprotocol/wallet';
|
import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||||
import {
|
import {
|
||||||
useVegaTransactionManager,
|
useVegaTransactionManager,
|
||||||
useVegaTransactionUpdater,
|
useVegaTransactionUpdater,
|
||||||
@@ -96,9 +96,7 @@ const cache: InMemoryCacheConfig = {
|
|||||||
const Web3Container = ({
|
const Web3Container = ({
|
||||||
chainId,
|
chainId,
|
||||||
}: {
|
}: {
|
||||||
/** Ethereum chain id */
|
|
||||||
chainId: number;
|
chainId: number;
|
||||||
/** Ethereum provider url */
|
|
||||||
providerUrl: string;
|
providerUrl: string;
|
||||||
}) => {
|
}) => {
|
||||||
const InitializeHandlers = () => {
|
const InitializeHandlers = () => {
|
||||||
@@ -125,9 +123,6 @@ const Web3Container = ({
|
|||||||
MOZILLA_EXTENSION_URL,
|
MOZILLA_EXTENSION_URL,
|
||||||
VEGA_WALLET_URL,
|
VEGA_WALLET_URL,
|
||||||
} = useEnvironment();
|
} = useEnvironment();
|
||||||
|
|
||||||
const vegaChainId = useChainId(VEGA_URL);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chainId) {
|
if (chainId) {
|
||||||
return initializeConnectors(
|
return initializeConnectors(
|
||||||
@@ -162,8 +157,7 @@ const Web3Container = ({
|
|||||||
!VEGA_EXPLORER_URL ||
|
!VEGA_EXPLORER_URL ||
|
||||||
!DocsLinks ||
|
!DocsLinks ||
|
||||||
!CHROME_EXTENSION_URL ||
|
!CHROME_EXTENSION_URL ||
|
||||||
!MOZILLA_EXTENSION_URL ||
|
!MOZILLA_EXTENSION_URL
|
||||||
!vegaChainId
|
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -175,7 +169,6 @@ const Web3Container = ({
|
|||||||
config={{
|
config={{
|
||||||
network: VEGA_ENV,
|
network: VEGA_ENV,
|
||||||
vegaUrl: VEGA_URL,
|
vegaUrl: VEGA_URL,
|
||||||
chainId: vegaChainId,
|
|
||||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||||
links: {
|
links: {
|
||||||
explorer: VEGA_EXPLORER_URL,
|
explorer: VEGA_EXPLORER_URL,
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import type { ApolloError } from '@apollo/client';
|
|
||||||
import {
|
|
||||||
PARTY_NOT_FOUND,
|
|
||||||
filterAcceptableGraphqlErrors,
|
|
||||||
isPartyNotFoundError,
|
|
||||||
} from './party';
|
|
||||||
import type { GraphQLError } from 'graphql';
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @returns GraphQLError
|
|
||||||
*/
|
|
||||||
function createMockApolloErrors(message: string): GraphQLError {
|
|
||||||
return {
|
|
||||||
message,
|
|
||||||
extensions: {
|
|
||||||
code: message.toUpperCase().replace(/ /g, '_'),
|
|
||||||
},
|
|
||||||
locations: [],
|
|
||||||
originalError: new Error(message),
|
|
||||||
path: [],
|
|
||||||
nodes: [],
|
|
||||||
positions: [1],
|
|
||||||
name: message,
|
|
||||||
source: {
|
|
||||||
body: message,
|
|
||||||
name: message,
|
|
||||||
locationOffset: {
|
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('filterAcceptableGraphqlErrors', () => {
|
|
||||||
it('should return undefined if the error is a party not found error', () => {
|
|
||||||
const error: Partial<ApolloError> = {
|
|
||||||
graphQLErrors: [createMockApolloErrors('failed to get party for ID')],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = filterAcceptableGraphqlErrors(error as ApolloError);
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the error if it is not a party not found error', () => {
|
|
||||||
const error: Partial<ApolloError> = {
|
|
||||||
graphQLErrors: [createMockApolloErrors('Some other error')],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = filterAcceptableGraphqlErrors(error as ApolloError);
|
|
||||||
|
|
||||||
expect(result).toEqual(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the error if there are multiple errors', () => {
|
|
||||||
const error: Partial<ApolloError> = {
|
|
||||||
graphQLErrors: [
|
|
||||||
createMockApolloErrors('failed to get party for ID'),
|
|
||||||
createMockApolloErrors('Some other error'),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = filterAcceptableGraphqlErrors(error as ApolloError);
|
|
||||||
|
|
||||||
expect(result).toEqual(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the error if there are no errors', () => {
|
|
||||||
const error: Partial<ApolloError> = {
|
|
||||||
graphQLErrors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = filterAcceptableGraphqlErrors(error as ApolloError);
|
|
||||||
|
|
||||||
expect(result).toEqual(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined if the error is undefined', () => {
|
|
||||||
const result = filterAcceptableGraphqlErrors(undefined);
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isPartyNotFoundError', () => {
|
|
||||||
it('should return true if the error message includes PARTY_NOT_FOUND', () => {
|
|
||||||
const error = { message: 'failed to get party for ID' };
|
|
||||||
|
|
||||||
const result = isPartyNotFoundError(error);
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if the error message does not include PARTY_NOT_FOUND', () => {
|
|
||||||
const error = { message: 'Some other error' };
|
|
||||||
|
|
||||||
const result = isPartyNotFoundError(error);
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Will trip if the error message changes, which should not be a problem, but there
|
|
||||||
// might be logic that depends on it
|
|
||||||
it('expects party not found error to remain consistent', () => {
|
|
||||||
const error = 'failed to get party for ID';
|
|
||||||
|
|
||||||
expect(PARTY_NOT_FOUND).toStrictEqual(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { ApolloError } from '@apollo/client';
|
|
||||||
|
|
||||||
export const PARTY_NOT_FOUND = 'failed to get party for ID';
|
export const PARTY_NOT_FOUND = 'failed to get party for ID';
|
||||||
|
|
||||||
export const isPartyNotFoundError = (error: { message: string }) => {
|
export const isPartyNotFoundError = (error: { message: string }) => {
|
||||||
@@ -8,23 +6,3 @@ export const isPartyNotFoundError = (error: { message: string }) => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* If a party has no accounts or data, then this GraphQL query believes it does not exist
|
|
||||||
* Not having any rewards is a valid state, so in some cases we can filter this error out.
|
|
||||||
*
|
|
||||||
* @param error ApolloError | undefined
|
|
||||||
* @returns ApolloError | undefined
|
|
||||||
*/
|
|
||||||
export function filterAcceptableGraphqlErrors(
|
|
||||||
error?: ApolloError
|
|
||||||
): ApolloError | undefined {
|
|
||||||
// Currently the only error we expect is when a party has no accounts
|
|
||||||
if (error && error.graphQLErrors.length === 1) {
|
|
||||||
if (isPartyNotFoundError(error.graphQLErrors[0])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -124,7 +124,7 @@ describe('Proposal header', () => {
|
|||||||
screen.queryByTestId('proposal-description')
|
screen.queryByTestId('proposal-description')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||||
'Update to market ID: MarketId'
|
'Market change: MarketId'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
-42
@@ -1,11 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||||
CopyWithTooltip,
|
|
||||||
Lozenge,
|
|
||||||
Tooltip,
|
|
||||||
VegaIcon,
|
|
||||||
VegaIconNames,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { shorten } from '@vegaprotocol/utils';
|
import { shorten } from '@vegaprotocol/utils';
|
||||||
import { Heading, SubHeading } from '../../../../components/heading';
|
import { Heading, SubHeading } from '../../../../components/heading';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
@@ -18,12 +12,7 @@ import {
|
|||||||
useNewTransferProposalDetails,
|
useNewTransferProposalDetails,
|
||||||
useSuccessorMarketProposalDetails,
|
useSuccessorMarketProposalDetails,
|
||||||
} from '@vegaprotocol/proposals';
|
} from '@vegaprotocol/proposals';
|
||||||
import {
|
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||||
CONSOLE_MARKET_PAGE,
|
|
||||||
DApp,
|
|
||||||
useFeatureFlags,
|
|
||||||
useLinks,
|
|
||||||
} from '@vegaprotocol/environment';
|
|
||||||
import Routes from '../../../routes';
|
import Routes from '../../../routes';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { VoteState } from '../vote-details/use-user-vote';
|
import type { VoteState } from '../vote-details/use-user-vote';
|
||||||
@@ -43,8 +32,6 @@ export const ProposalHeader = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const change = proposal?.terms.change;
|
const change = proposal?.terms.change;
|
||||||
|
|
||||||
const consoleLink = useLinks(DApp.Console);
|
|
||||||
|
|
||||||
let details: ReactNode;
|
let details: ReactNode;
|
||||||
let proposalType = '';
|
let proposalType = '';
|
||||||
let fallbackTitle = '';
|
let fallbackTitle = '';
|
||||||
@@ -119,33 +106,8 @@ export const ProposalHeader = ({
|
|||||||
fallbackTitle = t('UpdateMarketProposal');
|
fallbackTitle = t('UpdateMarketProposal');
|
||||||
details = (
|
details = (
|
||||||
<>
|
<>
|
||||||
<span>{t('UpdateToMarket')}:</span>{' '}
|
<span>{t('MarketChange')}:</span>{' '}
|
||||||
<span className="inline-flex items-start gap-2">
|
<span>{truncateMiddle(change.marketId)}</span>
|
||||||
<span className="break-all">{change.marketId} </span>
|
|
||||||
<span className="inline-flex items-end gap-0">
|
|
||||||
<CopyWithTooltip
|
|
||||||
text={change.marketId}
|
|
||||||
description={t('copyToClipboard')}
|
|
||||||
>
|
|
||||||
<button className="inline-block px-1">
|
|
||||||
<VegaIcon size={20} name={VegaIconNames.COPY} />
|
|
||||||
</button>
|
|
||||||
</CopyWithTooltip>
|
|
||||||
<Tooltip description={t('OpenInConsole')} align="center">
|
|
||||||
<button
|
|
||||||
className="inline-block px-1"
|
|
||||||
onClick={() => {
|
|
||||||
const marketPageLink = consoleLink(
|
|
||||||
CONSOLE_MARKET_PAGE.replace(':marketId', change.marketId)
|
|
||||||
);
|
|
||||||
window.open(marketPageLink, '_blank');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const vegaWalletConfig: VegaWalletConfig = {
|
|||||||
chromeExtensionUrl: 'chrome',
|
chromeExtensionUrl: 'chrome',
|
||||||
mozillaExtensionUrl: 'mozilla',
|
mozillaExtensionUrl: 'mozilla',
|
||||||
},
|
},
|
||||||
chainId: 'VEGA_CHAIN_ID',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderComponent = (proposal: ProposalQuery['proposal']) => {
|
const renderComponent = (proposal: ProposalQuery['proposal']) => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { ProposalMinRequirements, ProposalUserAction } from '../shared';
|
|||||||
import { VoteTransactionDialog } from './vote-transaction-dialog';
|
import { VoteTransactionDialog } from './vote-transaction-dialog';
|
||||||
import { useVoteButtonsQuery } from './__generated__/Stake';
|
import { useVoteButtonsQuery } from './__generated__/Stake';
|
||||||
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
|
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
|
||||||
import { filterAcceptableGraphqlErrors } from '../../../../lib/party';
|
|
||||||
|
|
||||||
interface VoteButtonsContainerProps {
|
interface VoteButtonsContainerProps {
|
||||||
voteState: VoteState | null;
|
voteState: VoteState | null;
|
||||||
@@ -43,10 +42,8 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
|
|||||||
skip: !pubKey,
|
skip: !pubKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredErrors = filterAcceptableGraphqlErrors(error);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={filteredErrors} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
<VoteButtons
|
<VoteButtons
|
||||||
{...props}
|
{...props}
|
||||||
currentStakeAvailable={toBigNum(
|
currentStakeAvailable={toBigNum(
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ query Proposal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
liquidityMonitoringParameters {
|
liquidityMonitoringParameters {
|
||||||
|
triggeringRatio
|
||||||
targetStakeParameters {
|
targetStakeParameters {
|
||||||
timeWindow
|
timeWindow
|
||||||
scalingFactor
|
scalingFactor
|
||||||
@@ -212,6 +213,7 @@ query Proposal(
|
|||||||
}
|
}
|
||||||
positionDecimalPlaces
|
positionDecimalPlaces
|
||||||
linearSlippageFactor
|
linearSlippageFactor
|
||||||
|
quadraticSlippageFactor
|
||||||
}
|
}
|
||||||
... on UpdateMarket {
|
... on UpdateMarket {
|
||||||
marketId
|
marketId
|
||||||
@@ -364,6 +366,7 @@ query Proposal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
liquidityMonitoringParameters {
|
liquidityMonitoringParameters {
|
||||||
|
triggeringRatio
|
||||||
targetStakeParameters {
|
targetStakeParameters {
|
||||||
timeWindow
|
timeWindow
|
||||||
scalingFactor
|
scalingFactor
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+13
-23
@@ -10,7 +10,6 @@ import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
|
|||||||
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
|
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
|
||||||
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
|
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
|
||||||
import { useNetworkParam } from '@vegaprotocol/network-parameters';
|
import { useNetworkParam } from '@vegaprotocol/network-parameters';
|
||||||
import { filterAcceptableGraphqlErrors } from '../../../lib/party';
|
|
||||||
|
|
||||||
const EPOCHS_PAGE_SIZE = 10;
|
const EPOCHS_PAGE_SIZE = 10;
|
||||||
|
|
||||||
@@ -100,24 +99,17 @@ export const EpochIndividualRewards = ({
|
|||||||
prevEpochIdRef.current = epochId;
|
prevEpochIdRef.current = epochId;
|
||||||
}, [epochId, refetchData]);
|
}, [epochId, refetchData]);
|
||||||
|
|
||||||
// Workarounds for the error handling of AsyncRenderer
|
|
||||||
const filteredErrors = filterAcceptableGraphqlErrors(error);
|
|
||||||
const filteredData = data || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={filteredErrors}
|
error={error}
|
||||||
data={filteredData}
|
data={data}
|
||||||
render={() => (
|
render={() => (
|
||||||
<div>
|
<div>
|
||||||
<p data-testid="connected-vega-key" className="mb-10">
|
<p data-testid="connected-vega-key" className="mb-10">
|
||||||
{t('Connected Vega key')}:{' '}
|
{t('Connected Vega key')}:{' '}
|
||||||
<span className="text-white">{pubKey}</span>
|
<span className="text-white">{pubKey}</span>
|
||||||
</p>
|
</p>
|
||||||
{epochIndividualRewardSummaries.length === 0 && (
|
|
||||||
<p>{t('No rewards for key')}</p>
|
|
||||||
)}
|
|
||||||
{epochIndividualRewardSummaries.map(
|
{epochIndividualRewardSummaries.map(
|
||||||
(epochIndividualRewardSummary) => (
|
(epochIndividualRewardSummary) => (
|
||||||
<EpochIndividualRewardsTable
|
<EpochIndividualRewardsTable
|
||||||
@@ -126,19 +118,17 @@ export const EpochIndividualRewards = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{epochIndividualRewardSummaries.length > 0 && (
|
<Pagination
|
||||||
<Pagination
|
isLoading={loading}
|
||||||
isLoading={loading}
|
hasPrevPage={page > 1}
|
||||||
hasPrevPage={page > 1}
|
hasNextPage={page < totalPages}
|
||||||
hasNextPage={page < totalPages}
|
onBack={() => refetchData(page - 1)}
|
||||||
onBack={() => refetchData(page - 1)}
|
onNext={() => refetchData(page + 1)}
|
||||||
onNext={() => refetchData(page + 1)}
|
onFirst={() => refetchData(1)}
|
||||||
onFirst={() => refetchData(1)}
|
onLast={() => refetchData(totalPages)}
|
||||||
onLast={() => refetchData(totalPages)}
|
>
|
||||||
>
|
{t('Page')} {page}
|
||||||
{t('Page')} {page}
|
</Pagination>
|
||||||
</Pagination>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@ import type { ValidatorsView } from './validator-tables';
|
|||||||
const nodeFactory = (
|
const nodeFactory = (
|
||||||
overrides?: PartialDeep<NodesFragmentFragment>
|
overrides?: PartialDeep<NodesFragmentFragment>
|
||||||
): NodesFragmentFragment => {
|
): NodesFragmentFragment => {
|
||||||
const defaultNode: NodesFragmentFragment = {
|
const defaultNode = {
|
||||||
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||||
name: 'high',
|
name: 'high',
|
||||||
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
||||||
@@ -288,7 +288,7 @@ describe('Consensus validators table', () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
grid.querySelector('[role="gridcell"][col-id="totalPenalties"]')
|
grid.querySelector('[role="gridcell"][col-id="totalPenalties"]')
|
||||||
).toHaveTextContent('13.16%');
|
).toHaveTextContent('10.07%');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]')
|
grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]')
|
||||||
|
|||||||
+2
-11
@@ -185,15 +185,6 @@ export const ConsensusValidatorsTable = ({
|
|||||||
const { rawValidatorScore: previousEpochValidatorScore } =
|
const { rawValidatorScore: previousEpochValidatorScore } =
|
||||||
getLastEpochScoreAndPerformance(previousEpochData, id);
|
getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||||
|
|
||||||
const overstakingPenalty = calculateOverallPenalty(
|
|
||||||
id,
|
|
||||||
allNodesInPreviousEpoch
|
|
||||||
);
|
|
||||||
const totalPenalty = calculateOverstakedPenalty(
|
|
||||||
id,
|
|
||||||
allNodesInPreviousEpoch
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
|
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
|
||||||
@@ -222,11 +213,11 @@ export const ConsensusValidatorsTable = ({
|
|||||||
2
|
2
|
||||||
),
|
),
|
||||||
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
|
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
|
||||||
overstakingPenalty,
|
calculateOverstakedPenalty(id, allNodesInPreviousEpoch),
|
||||||
2
|
2
|
||||||
),
|
),
|
||||||
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
|
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
|
||||||
totalPenalty,
|
calculateOverallPenalty(id, allNodesInPreviousEpoch),
|
||||||
2
|
2
|
||||||
),
|
),
|
||||||
[ValidatorFields.PENDING_STAKE]: pendingStake,
|
[ValidatorFields.PENDING_STAKE]: pendingStake,
|
||||||
|
|||||||
+2
-11
@@ -124,15 +124,6 @@ export const StandbyPendingValidatorsTable = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const overstakingPenalty = calculateOverallPenalty(
|
|
||||||
id,
|
|
||||||
allNodesInPreviousEpoch
|
|
||||||
);
|
|
||||||
const totalPenalty = calculateOverstakedPenalty(
|
|
||||||
id,
|
|
||||||
allNodesInPreviousEpoch
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
|
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
|
||||||
@@ -163,11 +154,11 @@ export const StandbyPendingValidatorsTable = ({
|
|||||||
2
|
2
|
||||||
),
|
),
|
||||||
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
|
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
|
||||||
overstakingPenalty,
|
calculateOverstakedPenalty(id, allNodesInPreviousEpoch),
|
||||||
2
|
2
|
||||||
),
|
),
|
||||||
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
|
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
|
||||||
totalPenalty,
|
calculateOverallPenalty(id, allNodesInPreviousEpoch),
|
||||||
2
|
2
|
||||||
),
|
),
|
||||||
[ValidatorFields.PENDING_STAKE]: pendingStake,
|
[ValidatorFields.PENDING_STAKE]: pendingStake,
|
||||||
|
|||||||
@@ -266,9 +266,7 @@ export const ValidatorTable = ({
|
|||||||
|
|
||||||
<Tooltip description={t('OverstakedPenaltyDescription')}>
|
<Tooltip description={t('OverstakedPenaltyDescription')}>
|
||||||
<span data-testid="overstaking-penalty">
|
<span data-testid="overstaking-penalty">
|
||||||
{penalties.overstaked
|
{formatNumberPercentage(penalties.overstaked, 2)}
|
||||||
? formatNumberPercentage(penalties.overstaked, 2)
|
|
||||||
: '-'}
|
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</KeyValueTableRow>
|
</KeyValueTableRow>
|
||||||
@@ -287,9 +285,7 @@ export const ValidatorTable = ({
|
|||||||
</span>
|
</span>
|
||||||
<span data-testid="total-penalties">
|
<span data-testid="total-penalties">
|
||||||
<strong>
|
<strong>
|
||||||
{penalties.overall
|
{formatNumberPercentage(penalties.overall, 2)}
|
||||||
? formatNumberPercentage(penalties.overall, 2)
|
|
||||||
: '-'}
|
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
</KeyValueTableRow>
|
</KeyValueTableRow>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import {
|
|||||||
getLastEpochScoreAndPerformance,
|
getLastEpochScoreAndPerformance,
|
||||||
getNormalisedVotingPower,
|
getNormalisedVotingPower,
|
||||||
getUnnormalisedVotingPower,
|
getUnnormalisedVotingPower,
|
||||||
|
getOverstakingPenalty,
|
||||||
getFormattedPerformanceScore,
|
getFormattedPerformanceScore,
|
||||||
getPerformancePenalty,
|
getPerformancePenalty,
|
||||||
|
getTotalPenalties,
|
||||||
getStakePercentage,
|
getStakePercentage,
|
||||||
calculateOverallPenalty,
|
|
||||||
calculateOverstakedPenalty,
|
|
||||||
} from './shared';
|
} from './shared';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
|
||||||
@@ -106,6 +106,38 @@ describe('getUnnormalisedVotingPower', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getOverstakingPenalty', () => {
|
||||||
|
it('returns "0%" when both arguments are null or undefined', () => {
|
||||||
|
expect(getOverstakingPenalty(null, null)).toBe('0%');
|
||||||
|
expect(getOverstakingPenalty(undefined, undefined)).toBe('0%');
|
||||||
|
expect(getOverstakingPenalty(null, undefined)).toBe('0%');
|
||||||
|
expect(getOverstakingPenalty(undefined, null)).toBe('0%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "0%" when one argument is null or undefined', () => {
|
||||||
|
expect(getOverstakingPenalty('10', null)).toBe('0%');
|
||||||
|
expect(getOverstakingPenalty(null, '20')).toBe('0%');
|
||||||
|
expect(getOverstakingPenalty('10', undefined)).toBe('0%');
|
||||||
|
expect(getOverstakingPenalty(undefined, '20')).toBe('0%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "0%" when validatorScore or stakeScore is zero', () => {
|
||||||
|
expect(getOverstakingPenalty('0', '20')).toBe('0%');
|
||||||
|
expect(getOverstakingPenalty('10', '0')).toBe('0%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct overstaking penalty', () => {
|
||||||
|
expect(getOverstakingPenalty('0.18', '0.2')).toBe('10.00%');
|
||||||
|
expect(getOverstakingPenalty('0.2', '0.2')).toBe('0.00%');
|
||||||
|
expect(getOverstakingPenalty('0.04', '0.2')).toBe('80.00%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles string numbers with decimals', () => {
|
||||||
|
expect(getOverstakingPenalty('7.5', '15')).toBe('50.00%');
|
||||||
|
expect(getOverstakingPenalty('12.5', '25')).toBe('50.00%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getFormattedPerformanceScore', () => {
|
describe('getFormattedPerformanceScore', () => {
|
||||||
it('should return the formatted performance score', () => {
|
it('should return the formatted performance score', () => {
|
||||||
expect(getFormattedPerformanceScore('0.25')).toEqual(new BigNumber(0.25));
|
expect(getFormattedPerformanceScore('0.25')).toEqual(new BigNumber(0.25));
|
||||||
@@ -120,6 +152,17 @@ describe('getPerformancePenalty', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getTotalPenalties', () => {
|
||||||
|
it('should return the total penalties', () => {
|
||||||
|
expect(getTotalPenalties('0.25', '1', '5000', '10000')).toEqual('50.00%');
|
||||||
|
expect(getTotalPenalties('0.25', '0.5', '5000', '10000')).toEqual('75.00%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 if the total penalties is negative', () => {
|
||||||
|
expect(getTotalPenalties('0.25', '0.5', '1000', '10000')).toEqual('0.00%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getStakePercentage', () => {
|
describe('getStakePercentage', () => {
|
||||||
it('should return the stake percentage', () => {
|
it('should return the stake percentage', () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -139,107 +182,3 @@ describe('getStakePercentage', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('calculateOverallPenalty', () => {
|
|
||||||
it('returns null if rewardScore is null', () => {
|
|
||||||
const res = calculateOverallPenalty('1', [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
rewardScore: null,
|
|
||||||
stakedTotal: '',
|
|
||||||
rankingScore: {
|
|
||||||
stakeScore: '0.25',
|
|
||||||
performanceScore: '0.75',
|
|
||||||
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
previousStatus:
|
|
||||||
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
rankingScore: '',
|
|
||||||
votingPower: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(res).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null if rewardScore.rawValidatorScore is null (should not happen)', () => {
|
|
||||||
const res = calculateOverallPenalty('1', [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
stakedTotal: '',
|
|
||||||
rewardScore: {
|
|
||||||
rawValidatorScore: '0.25',
|
|
||||||
performanceScore: '0.75',
|
|
||||||
multisigScore: '',
|
|
||||||
validatorScore: null as unknown as string,
|
|
||||||
normalisedScore: '',
|
|
||||||
validatorStatus:
|
|
||||||
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
},
|
|
||||||
rankingScore: {
|
|
||||||
stakeScore: '0.25',
|
|
||||||
performanceScore: '0.75',
|
|
||||||
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
previousStatus:
|
|
||||||
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
rankingScore: '',
|
|
||||||
votingPower: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(res).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('calculateOverstakedPenalty', () => {
|
|
||||||
it('returns null if rewardScore is null', () => {
|
|
||||||
const res = calculateOverstakedPenalty('1', [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
rewardScore: null,
|
|
||||||
stakedTotal: '',
|
|
||||||
rankingScore: {
|
|
||||||
stakeScore: '0.25',
|
|
||||||
performanceScore: '0.75',
|
|
||||||
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
previousStatus:
|
|
||||||
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
rankingScore: '',
|
|
||||||
votingPower: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(res).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null if rewardScore.rawValidatorScore is null (should not happen)', () => {
|
|
||||||
const res = calculateOverstakedPenalty('1', [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
stakedTotal: '',
|
|
||||||
rewardScore: {
|
|
||||||
rawValidatorScore: null as unknown as string,
|
|
||||||
performanceScore: '0.75',
|
|
||||||
multisigScore: '',
|
|
||||||
validatorScore: '',
|
|
||||||
normalisedScore: '',
|
|
||||||
validatorStatus:
|
|
||||||
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
},
|
|
||||||
rankingScore: {
|
|
||||||
stakeScore: '0.25',
|
|
||||||
performanceScore: '0.75',
|
|
||||||
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
previousStatus:
|
|
||||||
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
rankingScore: '',
|
|
||||||
votingPower: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(res).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
import type { PreviousEpochQuery } from './__generated__/PreviousEpoch';
|
import type { PreviousEpochQuery } from './__generated__/PreviousEpoch';
|
||||||
import { BigNumber } from '../../lib/bignumber';
|
import { BigNumber } from '../../lib/bignumber';
|
||||||
import type { LastArrayElement } from 'type-fest';
|
import type { LastArrayElement } from 'type-fest';
|
||||||
import isNull from 'lodash/isNull';
|
|
||||||
|
|
||||||
type Node = NonNullable<
|
type Node = NonNullable<
|
||||||
LastArrayElement<
|
LastArrayElement<
|
||||||
@@ -22,10 +21,7 @@ type Node = NonNullable<
|
|||||||
* @returns Theoretical stake score for given node based on the staked total
|
* @returns Theoretical stake score for given node based on the staked total
|
||||||
* of all node of the same type (status)
|
* of all node of the same type (status)
|
||||||
*/
|
*/
|
||||||
const calculateTheoreticalStakeScore = (
|
const calculateTheoreticalStakeScore = (nodeId: string, nodes: Node[]) => {
|
||||||
nodeId: string,
|
|
||||||
nodes: Node[]
|
|
||||||
): BigNumber | null => {
|
|
||||||
const node = nodes.find((n) => n.id === nodeId);
|
const node = nodes.find((n) => n.id === nodeId);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return new BigNumber(0);
|
return new BigNumber(0);
|
||||||
@@ -46,25 +42,14 @@ const calculateTheoreticalStakeScore = (
|
|||||||
* @param nodes A collection of all nodes - needed to calculate theoretical stake score
|
* @param nodes A collection of all nodes - needed to calculate theoretical stake score
|
||||||
* @returns %
|
* @returns %
|
||||||
*/
|
*/
|
||||||
export const calculateOverallPenalty = (
|
export const calculateOverallPenalty = (nodeId: string, nodes: Node[]) => {
|
||||||
nodeId: string,
|
|
||||||
nodes: Node[]
|
|
||||||
): BigNumber | null => {
|
|
||||||
const node = nodes.find((n) => n.id === nodeId);
|
const node = nodes.find((n) => n.id === nodeId);
|
||||||
const tts = calculateTheoreticalStakeScore(nodeId, nodes);
|
const tts = calculateTheoreticalStakeScore(nodeId, nodes);
|
||||||
if (
|
if (!node || tts.isZero()) {
|
||||||
!node ||
|
|
||||||
isNull(tts) ||
|
|
||||||
!node.rewardScore ||
|
|
||||||
(node.rewardScore && isNull(node.rewardScore.validatorScore))
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (tts.isZero()) {
|
|
||||||
return new BigNumber(0);
|
return new BigNumber(0);
|
||||||
}
|
}
|
||||||
const penalty = new BigNumber(1)
|
const penalty = new BigNumber(1)
|
||||||
.minus(new BigNumber(node.rewardScore.validatorScore).dividedBy(tts))
|
.minus(new BigNumber(node.rewardScore?.validatorScore || 0).dividedBy(tts))
|
||||||
.times(100);
|
.times(100);
|
||||||
return penalty.isLessThan(0) ? new BigNumber(0) : penalty;
|
return penalty.isLessThan(0) ? new BigNumber(0) : penalty;
|
||||||
};
|
};
|
||||||
@@ -75,21 +60,10 @@ export const calculateOverallPenalty = (
|
|||||||
* @param nodes A collection of all nodes - needed to calculate theoretical stake score
|
* @param nodes A collection of all nodes - needed to calculate theoretical stake score
|
||||||
* @returns %
|
* @returns %
|
||||||
*/
|
*/
|
||||||
export const calculateOverstakedPenalty = (
|
export const calculateOverstakedPenalty = (nodeId: string, nodes: Node[]) => {
|
||||||
nodeId: string,
|
|
||||||
nodes: Node[]
|
|
||||||
): BigNumber | null => {
|
|
||||||
const node = nodes.find((n) => n.id === nodeId);
|
const node = nodes.find((n) => n.id === nodeId);
|
||||||
const tts = calculateTheoreticalStakeScore(nodeId, nodes);
|
const tts = calculateTheoreticalStakeScore(nodeId, nodes);
|
||||||
if (
|
if (!node || tts.isZero()) {
|
||||||
!node ||
|
|
||||||
isNull(tts) ||
|
|
||||||
isNull(node.rewardScore) ||
|
|
||||||
(node.rewardScore && node.rewardScore.rawValidatorScore === null)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (tts.isZero()) {
|
|
||||||
return new BigNumber(0);
|
return new BigNumber(0);
|
||||||
}
|
}
|
||||||
const penalty = new BigNumber(1)
|
const penalty = new BigNumber(1)
|
||||||
@@ -104,9 +78,7 @@ export const calculateOverstakedPenalty = (
|
|||||||
* Calculates performance penalty based on the given performance score.
|
* Calculates performance penalty based on the given performance score.
|
||||||
* @returns %
|
* @returns %
|
||||||
*/
|
*/
|
||||||
export const calculatesPerformancePenalty = (
|
export const calculatesPerformancePenalty = (performanceScore: string) => {
|
||||||
performanceScore: string
|
|
||||||
): BigNumber => {
|
|
||||||
const penalty = new BigNumber(1)
|
const penalty = new BigNumber(1)
|
||||||
.minus(new BigNumber(performanceScore))
|
.minus(new BigNumber(performanceScore))
|
||||||
.times(100);
|
.times(100);
|
||||||
@@ -151,6 +123,60 @@ export const getPerformancePenalty = (performanceScore?: string) =>
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getOverstakingPenalty = (
|
||||||
|
validatorScore: string | null | undefined,
|
||||||
|
stakeScore: string | null | undefined
|
||||||
|
) => {
|
||||||
|
if (!validatorScore || !stakeScore) {
|
||||||
|
return '0%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid division by zero
|
||||||
|
if (
|
||||||
|
new BigNumber(validatorScore).isZero() ||
|
||||||
|
new BigNumber(stakeScore).isZero()
|
||||||
|
) {
|
||||||
|
return '0%';
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatNumberPercentage(
|
||||||
|
BigNumber.max(
|
||||||
|
new BigNumber(1)
|
||||||
|
.minus(
|
||||||
|
new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore))
|
||||||
|
)
|
||||||
|
.times(100),
|
||||||
|
new BigNumber(0)
|
||||||
|
),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTotalPenalties = (
|
||||||
|
rawValidatorScore: string | null | undefined,
|
||||||
|
performanceScore: string | undefined,
|
||||||
|
stakedOnNode: string,
|
||||||
|
totalStake: string
|
||||||
|
) => {
|
||||||
|
const calc =
|
||||||
|
rawValidatorScore &&
|
||||||
|
performanceScore &&
|
||||||
|
new BigNumber(totalStake).isGreaterThan(0)
|
||||||
|
? new BigNumber(1).minus(
|
||||||
|
new BigNumber(performanceScore)
|
||||||
|
.times(new BigNumber(rawValidatorScore))
|
||||||
|
.dividedBy(
|
||||||
|
new BigNumber(stakedOnNode).dividedBy(new BigNumber(totalStake))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: new BigNumber(0);
|
||||||
|
|
||||||
|
return formatNumberPercentage(
|
||||||
|
calc.isPositive() ? calc.times(100) : new BigNumber(0),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getStakePercentage = (total: BigNumber, stakedOnNode: BigNumber) =>
|
export const getStakePercentage = (total: BigNumber, stakedOnNode: BigNumber) =>
|
||||||
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
|
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
|
||||||
? '0%'
|
? '0%'
|
||||||
|
|||||||
@@ -1,50 +1,14 @@
|
|||||||
import {
|
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||||
Intent,
|
|
||||||
ToastsContainer,
|
|
||||||
TradingButton,
|
|
||||||
VegaIcon,
|
|
||||||
VegaIconNames,
|
|
||||||
useToasts,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
|
||||||
import {
|
import {
|
||||||
useEthereumTransactionToasts,
|
useEthereumTransactionToasts,
|
||||||
useEthereumWithdrawApprovalsToasts,
|
useEthereumWithdrawApprovalsToasts,
|
||||||
useVegaTransactionToasts,
|
useVegaTransactionToasts,
|
||||||
useWalletDisconnectToastActions,
|
|
||||||
useWalletDisconnectedToasts,
|
|
||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const WalletDisconnectAdditionalContent = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { hideToast } = useWalletDisconnectToastActions();
|
|
||||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
|
||||||
(store) => store.openVegaWalletDialog
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<p className="mt-2">
|
|
||||||
<TradingButton
|
|
||||||
data-testid="connect-vega-wallet"
|
|
||||||
onClick={() => {
|
|
||||||
hideToast();
|
|
||||||
openVegaWalletDialog();
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
intent={Intent.Danger}
|
|
||||||
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
|
|
||||||
>
|
|
||||||
<span className="whitespace-nowrap uppercase">{t('Connect')}</span>
|
|
||||||
</TradingButton>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ToastsManager = () => {
|
export const ToastsManager = () => {
|
||||||
useVegaTransactionToasts();
|
useVegaTransactionToasts();
|
||||||
useEthereumTransactionToasts();
|
useEthereumTransactionToasts();
|
||||||
useEthereumWithdrawApprovalsToasts();
|
useEthereumWithdrawApprovalsToasts();
|
||||||
useWalletDisconnectedToasts(<WalletDisconnectAdditionalContent />);
|
|
||||||
|
|
||||||
const toasts = useToasts((store) => store.toasts);
|
const toasts = useToasts((store) => store.toasts);
|
||||||
return <ToastsContainer order="desc" toasts={toasts} />;
|
return <ToastsContainer order="desc" toasts={toasts} />;
|
||||||
|
|||||||
+2
-8
@@ -21,14 +21,8 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
NX_STOP_ORDERS=true
|
NX_STOP_ORDERS=true
|
||||||
NX_ISOLATED_MARGIN=true
|
|
||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
# NX_DISABLE_CLOSE_POSITION=false
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
NX_TENDERMINT_URL=https://be.vega.community
|
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
|
||||||
|
|
||||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
|
||||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
NX_STOP_ORDERS=false
|
NX_STOP_ORDERS=false
|
||||||
NX_ISOLATED_MARGIN=true
|
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=false
|
NX_METAMASK_SNAPS=false
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
NX_STOP_ORDERS=true
|
NX_STOP_ORDERS=true
|
||||||
NX_ISOLATED_MARGIN=true
|
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
NX_STOP_ORDERS=true
|
NX_STOP_ORDERS=true
|
||||||
NX_ISOLATED_MARGIN=false
|
|
||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
@@ -29,6 +28,3 @@ NX_REFERRALS=true
|
|||||||
|
|
||||||
NX_TENDERMINT_URL=https://be.vega.community
|
NX_TENDERMINT_URL=https://be.vega.community
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||||
|
|
||||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
|
||||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
NX_STOP_ORDERS=true
|
NX_STOP_ORDERS=true
|
||||||
NX_ISOLATED_MARGIN=false
|
|
||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=false
|
NX_METAMASK_SNAPS=false
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
NX_STOP_ORDERS=true
|
NX_STOP_ORDERS=true
|
||||||
NX_ISOLATED_MARGIN=true
|
|
||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
NX_STOP_ORDERS=true
|
NX_STOP_ORDERS=true
|
||||||
NX_ISOLATED_MARGIN=true
|
|
||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
|
|||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
NX_STOP_ORDERS=true
|
NX_STOP_ORDERS=true
|
||||||
NX_ISOLATED_MARGIN=true
|
|
||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=false
|
NX_METAMASK_SNAPS=false
|
||||||
|
|||||||
@@ -54,5 +54,3 @@ To run the UI automation tests with a mocked API, run:
|
|||||||
```bash
|
```bash
|
||||||
yarn nx run trading-e2e:e2e
|
yarn nx run trading-e2e:e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
To run tests with market sim please read [the readme](e2e/README.md).
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||||
@@ -72,6 +72,17 @@ export const MarketPage = () => {
|
|||||||
}
|
}
|
||||||
}, [update, lastMarketId, data?.id]);
|
}, [update, lastMarketId, data?.id]);
|
||||||
|
|
||||||
|
const pinnedAsset = data && getAsset(data);
|
||||||
|
|
||||||
|
const tradeView = useMemo(() => {
|
||||||
|
if (pinnedAsset) {
|
||||||
|
if (largeScreen) {
|
||||||
|
return <TradeGrid market={data} pinnedAsset={pinnedAsset} />;
|
||||||
|
}
|
||||||
|
return <TradePanels market={data} pinnedAsset={pinnedAsset} />;
|
||||||
|
}
|
||||||
|
}, [largeScreen, data, pinnedAsset]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
@@ -107,8 +118,6 @@ export const MarketPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinnedAsset = data && getAsset(data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TitleUpdater
|
<TitleUpdater
|
||||||
@@ -116,11 +125,7 @@ export const MarketPage = () => {
|
|||||||
marketName={data?.tradableInstrument.instrument.name}
|
marketName={data?.tradableInstrument.instrument.name}
|
||||||
decimalPlaces={data?.decimalPlaces}
|
decimalPlaces={data?.decimalPlaces}
|
||||||
/>
|
/>
|
||||||
{largeScreen ? (
|
{tradeView}
|
||||||
<TradeGrid market={data} pinnedAsset={pinnedAsset} />
|
|
||||||
) : (
|
|
||||||
<TradePanels market={data} pinnedAsset={pinnedAsset} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { memo, type ReactNode } from 'react';
|
import { memo } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import { LayoutPriority } from 'allotment';
|
import { LayoutPriority } from 'allotment';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
import { OracleBanner, useMarket } from '@vegaprotocol/markets';
|
||||||
import { type Market } from '@vegaprotocol/markets';
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import {
|
||||||
ResizableGrid,
|
ResizableGrid,
|
||||||
@@ -12,19 +13,31 @@ import {
|
|||||||
usePaneLayout,
|
usePaneLayout,
|
||||||
} from '../../components/resizable-grid';
|
} from '../../components/resizable-grid';
|
||||||
import { TradingViews } from './trade-views';
|
import { TradingViews } from './trade-views';
|
||||||
|
import {
|
||||||
|
MarketSuccessorBanner,
|
||||||
|
MarketSuccessorProposalBanner,
|
||||||
|
MarketTerminationBanner,
|
||||||
|
} from '../../components/market-banner';
|
||||||
|
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { MarketBanner } from '../../components/market-banner';
|
|
||||||
|
|
||||||
interface TradeGridProps {
|
interface TradeGridProps {
|
||||||
market: Market;
|
market: Market | null;
|
||||||
pinnedAsset?: PinnedAsset | undefined;
|
pinnedAsset?: PinnedAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainGrid = memo(
|
const MainGrid = memo(
|
||||||
({ market, pinnedAsset }: { market: Market; pinnedAsset?: PinnedAsset }) => {
|
({
|
||||||
|
marketId,
|
||||||
|
pinnedAsset,
|
||||||
|
}: {
|
||||||
|
marketId: string;
|
||||||
|
pinnedAsset?: PinnedAsset;
|
||||||
|
}) => {
|
||||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||||
const t = useT();
|
const t = useT();
|
||||||
|
const { data: market } = useMarket(marketId);
|
||||||
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
|
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
|
||||||
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
|
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
|
||||||
id: 'middle-1',
|
id: 'middle-1',
|
||||||
@@ -52,17 +65,17 @@ const MainGrid = memo(
|
|||||||
menu={<TradingViews.chart.menu />}
|
menu={<TradingViews.chart.menu />}
|
||||||
>
|
>
|
||||||
<ErrorBoundary feature="chart">
|
<ErrorBoundary feature="chart">
|
||||||
<TradingViews.chart.component marketId={market.id} />
|
<TradingViews.chart.component marketId={marketId} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="depth" name={t('Depth')}>
|
<Tab id="depth" name={t('Depth')}>
|
||||||
<ErrorBoundary feature="depth">
|
<ErrorBoundary feature="depth">
|
||||||
<TradingViews.depth.component marketId={market.id} />
|
<TradingViews.depth.component marketId={marketId} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="liquidity" name={t('Liquidity')}>
|
<Tab id="liquidity" name={t('Liquidity')}>
|
||||||
<ErrorBoundary feature="liquidity">
|
<ErrorBoundary feature="liquidity">
|
||||||
<TradingViews.liquidity.component marketId={market.id} />
|
<TradingViews.liquidity.component marketId={marketId} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
{market &&
|
{market &&
|
||||||
@@ -70,7 +83,7 @@ const MainGrid = memo(
|
|||||||
'Perpetual' ? (
|
'Perpetual' ? (
|
||||||
<Tab id="funding-history" name={t('Funding history')}>
|
<Tab id="funding-history" name={t('Funding history')}>
|
||||||
<ErrorBoundary feature="funding-history">
|
<ErrorBoundary feature="funding-history">
|
||||||
<TradingViews.funding.component marketId={market.id} />
|
<TradingViews.funding.component marketId={marketId} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -84,7 +97,7 @@ const MainGrid = memo(
|
|||||||
>
|
>
|
||||||
<ErrorBoundary feature="funding-payments">
|
<ErrorBoundary feature="funding-payments">
|
||||||
<TradingViews.fundingPayments.component
|
<TradingViews.fundingPayments.component
|
||||||
marketId={market.id}
|
marketId={marketId}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -100,7 +113,7 @@ const MainGrid = memo(
|
|||||||
<Tabs storageKey="console-trade-grid-main-right">
|
<Tabs storageKey="console-trade-grid-main-right">
|
||||||
<Tab id="orderbook" name={t('Orderbook')}>
|
<Tab id="orderbook" name={t('Orderbook')}>
|
||||||
<ErrorBoundary feature="orderbook">
|
<ErrorBoundary feature="orderbook">
|
||||||
<TradingViews.orderbook.component marketId={market.id} />
|
<TradingViews.orderbook.component marketId={marketId} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
@@ -109,7 +122,7 @@ const MainGrid = memo(
|
|||||||
settings={<TradingViews.trades.settings />}
|
settings={<TradingViews.trades.settings />}
|
||||||
>
|
>
|
||||||
<ErrorBoundary feature="trades">
|
<ErrorBoundary feature="trades">
|
||||||
<TradingViews.trades.component marketId={market.id} />
|
<TradingViews.trades.component marketId={marketId} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -212,6 +225,7 @@ const MainGrid = memo(
|
|||||||
MainGrid.displayName = 'MainGrid';
|
MainGrid.displayName = 'MainGrid';
|
||||||
|
|
||||||
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||||
|
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||||
const wrapperClasses = classNames(
|
const wrapperClasses = classNames(
|
||||||
'h-full grid',
|
'h-full grid',
|
||||||
'grid-rows-[min-content_1fr]'
|
'grid-rows-[min-content_1fr]'
|
||||||
@@ -220,10 +234,17 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={wrapperClasses}>
|
<div className={wrapperClasses}>
|
||||||
<div>
|
<div>
|
||||||
<MarketBanner market={market} />
|
{featureFlags.SUCCESSOR_MARKETS && (
|
||||||
|
<>
|
||||||
|
<MarketSuccessorBanner market={market} />
|
||||||
|
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MarketTerminationBanner market={market} />
|
||||||
|
<OracleBanner marketId={market?.id || ''} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 p-0.5">
|
<div className="min-h-0 p-0.5">
|
||||||
<MainGrid market={market} pinnedAsset={pinnedAsset} />
|
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||||
import { type Market } from '@vegaprotocol/markets';
|
import { type Market } from '@vegaprotocol/markets';
|
||||||
// TODO: handle oracle banner
|
import { OracleBanner } from '@vegaprotocol/markets';
|
||||||
// import { OracleBanner } from '@vegaprotocol/markets';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -12,17 +11,23 @@ import {
|
|||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { MarketBanner } from '../../components/market-banner';
|
import {
|
||||||
|
MarketSuccessorBanner,
|
||||||
|
MarketSuccessorProposalBanner,
|
||||||
|
MarketTerminationBanner,
|
||||||
|
} from '../../components/market-banner';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { type TradingView } from './trade-views';
|
import { type TradingView } from './trade-views';
|
||||||
import { TradingViews } from './trade-views';
|
import { TradingViews } from './trade-views';
|
||||||
|
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
interface TradePanelsProps {
|
interface TradePanelsProps {
|
||||||
market: Market;
|
market: Market | null;
|
||||||
pinnedAsset?: PinnedAsset;
|
pinnedAsset?: PinnedAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||||
|
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||||
const [view, setView] = useState<TradingView>('chart');
|
const [view, setView] = useState<TradingView>('chart');
|
||||||
const viewCfg = TradingViews[view];
|
const viewCfg = TradingViews[view];
|
||||||
|
|
||||||
@@ -71,7 +76,14 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||||
<div>
|
<div>
|
||||||
<MarketBanner market={market} />
|
{featureFlags.SUCCESSOR_MARKETS && (
|
||||||
|
<>
|
||||||
|
<MarketSuccessorBanner market={market} />
|
||||||
|
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MarketTerminationBanner market={market} />
|
||||||
|
<OracleBanner marketId={market?.id || ''} />
|
||||||
</div>
|
</div>
|
||||||
<div>{renderMenu()}</div>
|
<div>{renderMenu()}</div>
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ describe('Closed', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('display market actions', async () => {
|
it('display market actions', async () => {
|
||||||
// Use market with a successor Id as the actions dropdown will optionally
|
// Use market with a succcessor Id as the actions dropdown will optionally
|
||||||
// show a link to the successor market
|
// show a link to the successor market
|
||||||
const marketsWithSuccessorAndParent = [
|
const marketsWithSuccessorAndParent = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -137,8 +137,6 @@ const ClosedMarketsDataGrid = ({
|
|||||||
headerName: t('Market'),
|
headerName: t('Market'),
|
||||||
field: 'code',
|
field: 'code',
|
||||||
cellRenderer: 'MarketCodeCell',
|
cellRenderer: 'MarketCodeCell',
|
||||||
width: 150,
|
|
||||||
resizable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: t('Status'),
|
headerName: t('Status'),
|
||||||
@@ -282,7 +280,6 @@ const ClosedMarketsDataGrid = ({
|
|||||||
return (
|
return (
|
||||||
<AgGrid
|
<AgGrid
|
||||||
rowData={rowData}
|
rowData={rowData}
|
||||||
defaultColDef={COL_DEFS.default}
|
|
||||||
columnDefs={colDefs}
|
columnDefs={colDefs}
|
||||||
getRowId={({ data }) => data.id}
|
getRowId={({ data }) => data.id}
|
||||||
overlayNoRowsTemplate={error ? error.message : t('No markets')}
|
overlayNoRowsTemplate={error ? error.message : t('No markets')}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const defaultColDef = {
|
|||||||
filter: true,
|
filter: true,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
filterParams: { buttons: ['reset'] },
|
filterParams: { buttons: ['reset'] },
|
||||||
minWidth: 120,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export const MarketsPage = () => {
|
|||||||
size="extra-small"
|
size="extra-small"
|
||||||
data-testid="propose-new-market"
|
data-testid="propose-new-market"
|
||||||
href={externalLink}
|
href={externalLink}
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
{t('Propose a new market')}
|
{t('Propose a new market')}
|
||||||
</TradingAnchorButton>
|
</TradingAnchorButton>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export const useColumnDefs = () => {
|
|||||||
{
|
{
|
||||||
headerName: t('Market'),
|
headerName: t('Market'),
|
||||||
field: 'tradableInstrument.instrument.code',
|
field: 'tradableInstrument.instrument.code',
|
||||||
pinned: true,
|
|
||||||
cellRenderer: ({
|
cellRenderer: ({
|
||||||
value,
|
value,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useT } from '../../lib/use-t';
|
|||||||
import { RewardsContainer } from '../../components/rewards-container';
|
import { RewardsContainer } from '../../components/rewards-container';
|
||||||
import { usePageTitleStore } from '../../stores';
|
import { usePageTitleStore } from '../../stores';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
export const Rewards = () => {
|
export const Rewards = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@@ -17,10 +16,10 @@ export const Rewards = () => {
|
|||||||
}, [updateTitle, title]);
|
}, [updateTitle, title]);
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary feature="rewards">
|
<ErrorBoundary feature="rewards">
|
||||||
<TinyScroll className="p-4 max-h-full overflow-auto">
|
<div className="container mx-auto p-4">
|
||||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
||||||
<RewardsContainer />
|
<RewardsContainer />
|
||||||
</TinyScroll>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Web3Provider } from './web3-provider';
|
import { Web3Provider } from './web3-provider';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { DataLoader } from './data-loader';
|
|
||||||
import { useChainId } from '@vegaprotocol/wallet';
|
|
||||||
|
|
||||||
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@@ -27,16 +25,13 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
|||||||
CHROME_EXTENSION_URL,
|
CHROME_EXTENSION_URL,
|
||||||
} = useEnvironment();
|
} = useEnvironment();
|
||||||
|
|
||||||
const chainId = useChainId(VEGA_URL);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!VEGA_URL ||
|
!VEGA_URL ||
|
||||||
!VEGA_WALLET_URL ||
|
!VEGA_WALLET_URL ||
|
||||||
!VEGA_EXPLORER_URL ||
|
!VEGA_EXPLORER_URL ||
|
||||||
!CHROME_EXTENSION_URL ||
|
!CHROME_EXTENSION_URL ||
|
||||||
!MOZILLA_EXTENSION_URL ||
|
!MOZILLA_EXTENSION_URL ||
|
||||||
!DocsLinks ||
|
!DocsLinks
|
||||||
!chainId
|
|
||||||
) {
|
) {
|
||||||
return <AppLoader />;
|
return <AppLoader />;
|
||||||
}
|
}
|
||||||
@@ -57,39 +52,28 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DataLoader
|
<Web3Provider
|
||||||
skeleton={<AppLoader />}
|
skeleton={<AppLoader />}
|
||||||
failure={
|
failure={
|
||||||
<AppFailure
|
<AppFailure title={t('Could not configure web3 provider')} />
|
||||||
title={t('Could not load market data or asset data')}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Web3Provider
|
<VegaWalletProvider
|
||||||
skeleton={<AppLoader />}
|
config={{
|
||||||
failure={
|
network: VEGA_ENV,
|
||||||
<AppFailure title={t('Could not configure web3 provider')} />
|
vegaUrl: VEGA_URL,
|
||||||
}
|
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||||
|
links: {
|
||||||
|
explorer: VEGA_EXPLORER_URL,
|
||||||
|
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
||||||
|
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
||||||
|
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<VegaWalletProvider
|
{children}
|
||||||
config={{
|
</VegaWalletProvider>
|
||||||
network: VEGA_ENV,
|
</Web3Provider>
|
||||||
vegaUrl: VEGA_URL,
|
|
||||||
chainId,
|
|
||||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
|
||||||
links: {
|
|
||||||
explorer: VEGA_EXPLORER_URL,
|
|
||||||
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
|
||||||
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
|
||||||
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</VegaWalletProvider>
|
|
||||||
</Web3Provider>
|
|
||||||
</DataLoader>
|
|
||||||
</NodeGuard>
|
</NodeGuard>
|
||||||
</NetworkLoader>
|
</NetworkLoader>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
|
||||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export const DataLoader = ({
|
|
||||||
children,
|
|
||||||
failure,
|
|
||||||
skeleton,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
failure: ReactNode;
|
|
||||||
skeleton: ReactNode;
|
|
||||||
}) => {
|
|
||||||
// Query all markets and assets to ensure they are cached
|
|
||||||
const { data: markets, error, loading } = useMarketsMapProvider();
|
|
||||||
const {
|
|
||||||
data: assets,
|
|
||||||
error: errorAssets,
|
|
||||||
loading: loadingAssets,
|
|
||||||
} = useAssetsMapProvider();
|
|
||||||
|
|
||||||
if (loading || loadingAssets) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
return <>{skeleton}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error || errorAssets || !markets || !assets) {
|
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
||||||
return <>{failure}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
||||||
return <>{children}</>;
|
|
||||||
};
|
|
||||||
@@ -25,12 +25,12 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
|||||||
overlays,
|
overlays,
|
||||||
studies,
|
studies,
|
||||||
studySizes,
|
studySizes,
|
||||||
|
tradingViewStudies,
|
||||||
setInterval,
|
setInterval,
|
||||||
setStudies,
|
setStudies,
|
||||||
setStudySizes,
|
setStudySizes,
|
||||||
setOverlays,
|
setOverlays,
|
||||||
state,
|
setTradingViewStudies,
|
||||||
setState,
|
|
||||||
} = useChartSettings();
|
} = useChartSettings();
|
||||||
|
|
||||||
const pennantChart = (
|
const pennantChart = (
|
||||||
@@ -64,13 +64,13 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
|||||||
libraryHash={CHARTING_LIBRARY_HASH}
|
libraryHash={CHARTING_LIBRARY_HASH}
|
||||||
marketId={marketId}
|
marketId={marketId}
|
||||||
interval={toTradingViewResolution(interval)}
|
interval={toTradingViewResolution(interval)}
|
||||||
|
studies={tradingViewStudies}
|
||||||
onIntervalChange={(newInterval) => {
|
onIntervalChange={(newInterval) => {
|
||||||
setInterval(fromTradingViewResolution(newInterval));
|
setInterval(fromTradingViewResolution(newInterval));
|
||||||
}}
|
}}
|
||||||
onAutoSaveNeeded={(data) => {
|
onAutoSaveNeeded={(data: { studies: string[] }) => {
|
||||||
setState(data);
|
setTradingViewStudies(data.studies);
|
||||||
}}
|
}}
|
||||||
state={state}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ describe('ChartMenu', () => {
|
|||||||
|
|
||||||
render(<ChartMenu />);
|
render(<ChartMenu />);
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
|
await userEvent.click(screen.getByRole('button', { name: 'TradingView' }));
|
||||||
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
|
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
|
await userEvent.click(screen.getByRole('button', { name: 'Vega chart' }));
|
||||||
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
|
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export const ChartMenu = () => {
|
|||||||
setChartlib(isPennant ? 'tradingview' : 'pennant');
|
setChartlib(isPennant ? 'tradingview' : 'pennant');
|
||||||
}}
|
}}
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
testId="chartlib-toggle-button"
|
|
||||||
>
|
>
|
||||||
{isPennant ? 'TradingView' : t('Vega chart')}
|
{isPennant ? 'TradingView' : t('Vega chart')}
|
||||||
</TradingButton>
|
</TradingButton>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ type StudySizes = { [S in Study]?: number };
|
|||||||
export type Chartlib = 'pennant' | 'tradingview';
|
export type Chartlib = 'pennant' | 'tradingview';
|
||||||
|
|
||||||
interface StoredSettings {
|
interface StoredSettings {
|
||||||
state: object | undefined; // Don't see a better type provided from TradingView type definitions
|
|
||||||
chartlib: Chartlib;
|
chartlib: Chartlib;
|
||||||
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
|
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
|
||||||
// chart types easier and more consistent
|
// chart types easier and more consistent
|
||||||
@@ -18,6 +17,7 @@ interface StoredSettings {
|
|||||||
overlays: Overlay[];
|
overlays: Overlay[];
|
||||||
studies: Study[];
|
studies: Study[];
|
||||||
studySizes: StudySizes;
|
studySizes: StudySizes;
|
||||||
|
tradingViewStudies: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STUDY_SIZE = 90;
|
export const STUDY_SIZE = 90;
|
||||||
@@ -30,13 +30,13 @@ const STUDY_ORDER: Study[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_CHART_SETTINGS = {
|
export const DEFAULT_CHART_SETTINGS = {
|
||||||
state: undefined,
|
|
||||||
chartlib: 'pennant' as const,
|
chartlib: 'pennant' as const,
|
||||||
interval: Interval.INTERVAL_I15M,
|
interval: Interval.INTERVAL_I15M,
|
||||||
type: ChartType.CANDLE,
|
type: ChartType.CANDLE,
|
||||||
overlays: [Overlay.MOVING_AVERAGE],
|
overlays: [Overlay.MOVING_AVERAGE],
|
||||||
studies: [Study.MACD, Study.VOLUME],
|
studies: [Study.MACD, Study.VOLUME],
|
||||||
studySizes: {},
|
studySizes: {},
|
||||||
|
tradingViewStudies: ['Volume'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useChartSettingsStore = create<
|
export const useChartSettingsStore = create<
|
||||||
@@ -47,7 +47,7 @@ export const useChartSettingsStore = create<
|
|||||||
setStudies: (studies?: Study[]) => void;
|
setStudies: (studies?: Study[]) => void;
|
||||||
setStudySizes: (sizes: number[]) => void;
|
setStudySizes: (sizes: number[]) => void;
|
||||||
setChartlib: (lib: Chartlib) => void;
|
setChartlib: (lib: Chartlib) => void;
|
||||||
setState: (state: object) => void;
|
setTradingViewStudies: (studies: string[]) => void;
|
||||||
}
|
}
|
||||||
>()(
|
>()(
|
||||||
persist(
|
persist(
|
||||||
@@ -95,8 +95,10 @@ export const useChartSettingsStore = create<
|
|||||||
state.chartlib = lib;
|
state.chartlib = lib;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setState: (state) => {
|
setTradingViewStudies: (studies: string[]) => {
|
||||||
set({ state });
|
set((state) => {
|
||||||
|
state.tradingViewStudies = studies;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
@@ -145,13 +147,13 @@ export const useChartSettings = () => {
|
|||||||
overlays,
|
overlays,
|
||||||
studies,
|
studies,
|
||||||
studySizes,
|
studySizes,
|
||||||
|
tradingViewStudies: settings.tradingViewStudies,
|
||||||
setInterval: settings.setInterval,
|
setInterval: settings.setInterval,
|
||||||
setType: settings.setType,
|
setType: settings.setType,
|
||||||
setStudies: settings.setStudies,
|
setStudies: settings.setStudies,
|
||||||
setOverlays: settings.setOverlays,
|
setOverlays: settings.setOverlays,
|
||||||
setStudySizes: settings.setStudySizes,
|
setStudySizes: settings.setStudySizes,
|
||||||
setChartlib: settings.setChartlib,
|
setChartlib: settings.setChartlib,
|
||||||
state: settings.state,
|
setTradingViewStudies: settings.setTradingViewStudies,
|
||||||
setState: settings.setState,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ export const FeesContainer = () => {
|
|||||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||||
|
|
||||||
const { data: programData, loading: programLoading } =
|
const { data: programData, loading: programLoading } =
|
||||||
useDiscountProgramsQuery({
|
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
|
||||||
errorPolicy: 'ignore',
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
pollInterval: 15000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const volumeDiscountWindowLength =
|
const volumeDiscountWindowLength =
|
||||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||||
@@ -53,8 +49,6 @@ export const FeesContainer = () => {
|
|||||||
partyId: pubKey || '',
|
partyId: pubKey || '',
|
||||||
},
|
},
|
||||||
skip: !pubKey,
|
skip: !pubKey,
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
pollInterval: 15000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||||
|
|
||||||
export const useReferralStats = (
|
export const useReferralStats = (
|
||||||
previousEpoch?: number,
|
previousEpoch?: number,
|
||||||
referralStats?: NonNullable<
|
referralStats?: NonNullable<
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { GridSettings } from './grid-settings';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
|
|
||||||
describe('GridSettings', () => {
|
|
||||||
it('calls updateGridStore with correct arguments on button click', async () => {
|
|
||||||
const mockUpdateGridStore = jest.fn();
|
|
||||||
const { getByText } = render(
|
|
||||||
<GridSettings updateGridStore={mockUpdateGridStore} />
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(getByText('Reset Columns'));
|
|
||||||
|
|
||||||
expect(mockUpdateGridStore).toHaveBeenCalledWith({
|
|
||||||
columnState: undefined,
|
|
||||||
filterModel: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -42,9 +42,13 @@ export const LiquidityHeader = () => {
|
|||||||
const assetDecimalPlaces = asset?.decimals || 0;
|
const assetDecimalPlaces = asset?.decimals || 0;
|
||||||
const symbol = asset?.symbol;
|
const symbol = asset?.symbol;
|
||||||
|
|
||||||
|
const triggeringRatio =
|
||||||
|
market?.liquidityMonitoringParameters.triggeringRatio || '1';
|
||||||
|
|
||||||
const { percentage, status } = useCheckLiquidityStatus({
|
const { percentage, status } = useCheckLiquidityStatus({
|
||||||
suppliedStake: suppliedStake || 0,
|
suppliedStake: suppliedStake || 0,
|
||||||
targetStake: targetStake || 0,
|
targetStake: targetStake || 0,
|
||||||
|
triggeringRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
const feesObject = feesPaidRes?.paidLiquidityFees?.edges?.find(
|
const feesObject = feesPaidRes?.paidLiquidityFees?.edges?.find(
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ export const MarketLiquiditySupplied = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const stakeToCcyVolume = params.market_liquidity_stakeToCcyVolume;
|
const stakeToCcyVolume = params.market_liquidity_stakeToCcyVolume;
|
||||||
|
const triggeringRatio = Number(
|
||||||
|
params.market_liquidity_targetstake_triggering_ratio
|
||||||
|
);
|
||||||
|
|
||||||
const variables = useMemo(
|
const variables = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -91,6 +94,7 @@ export const MarketLiquiditySupplied = ({
|
|||||||
const { percentage, status } = useCheckLiquidityStatus({
|
const { percentage, status } = useCheckLiquidityStatus({
|
||||||
suppliedStake: market?.suppliedStake || 0,
|
suppliedStake: market?.suppliedStake || 0,
|
||||||
targetStake: market?.targetStake || 0,
|
targetStake: market?.targetStake || 0,
|
||||||
|
triggeringRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showMessage =
|
const showMessage =
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export { MarketBanner } from './market-banner';
|
export * from './market-successor-banner';
|
||||||
|
export * from './market-successor-proposal-banner';
|
||||||
|
export * from './market-termination-banner';
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
import compact from 'lodash/compact';
|
|
||||||
import { MarketState } from '@vegaprotocol/types';
|
|
||||||
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import {
|
|
||||||
useMarketState,
|
|
||||||
type Market,
|
|
||||||
useMaliciousOracle,
|
|
||||||
} from '@vegaprotocol/markets';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
|
||||||
import { MarketSuspendedBanner } from './market-suspended-banner';
|
|
||||||
import { MarketUpdateBanner } from './market-update-banner';
|
|
||||||
import { MarketUpdateStateBanner } from './market-update-state-banner';
|
|
||||||
import { MarketSettledBanner } from './market-settled-banner';
|
|
||||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
|
||||||
import { MarketOracleBanner, type Oracle } from './market-oracle-banner';
|
|
||||||
import {
|
|
||||||
useSuccessorMarketProposals,
|
|
||||||
useUpdateMarketProposals,
|
|
||||||
useUpdateMarketStateProposals,
|
|
||||||
} from './use-market-proposals';
|
|
||||||
|
|
||||||
type UpdateMarketBanner = {
|
|
||||||
kind: 'UpdateMarket';
|
|
||||||
proposals: MarketViewProposalFieldsFragment[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdateMarketStateBanner = {
|
|
||||||
kind: 'UpdateMarketState';
|
|
||||||
proposals: MarketViewProposalFieldsFragment[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type NewMarketBanner = {
|
|
||||||
kind: 'NewMarket'; // aka a proposal of NewMarket which succeeds the current market
|
|
||||||
proposals: MarketViewProposalFieldsFragment[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type SettledBanner = {
|
|
||||||
kind: 'Settled';
|
|
||||||
market: Market;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SuspendedBanner = {
|
|
||||||
kind: 'Suspended';
|
|
||||||
market: Market;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OracleBanner = {
|
|
||||||
kind: 'Oracle';
|
|
||||||
oracle: Oracle;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Banner =
|
|
||||||
| UpdateMarketBanner
|
|
||||||
| UpdateMarketStateBanner
|
|
||||||
| NewMarketBanner
|
|
||||||
| SettledBanner
|
|
||||||
| SuspendedBanner
|
|
||||||
| OracleBanner;
|
|
||||||
|
|
||||||
export const MarketBanner = ({ market }: { market: Market }) => {
|
|
||||||
const { data: marketState } = useMarketState(market.id);
|
|
||||||
|
|
||||||
const { proposals: successorProposals, loading: successorLoading } =
|
|
||||||
useSuccessorMarketProposals(market.id);
|
|
||||||
|
|
||||||
const { proposals: updateMarketProposals, loading: updateMarketLoading } =
|
|
||||||
useUpdateMarketProposals(market.id);
|
|
||||||
|
|
||||||
const {
|
|
||||||
proposals: updateMarketStateProposals,
|
|
||||||
loading: updateMarketStateLoading,
|
|
||||||
} = useUpdateMarketStateProposals(market.id);
|
|
||||||
|
|
||||||
const { data: maliciousOracle, loading: oracleLoading } = useMaliciousOracle(
|
|
||||||
market.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const loading =
|
|
||||||
successorLoading ||
|
|
||||||
updateMarketLoading ||
|
|
||||||
updateMarketStateLoading ||
|
|
||||||
oracleLoading;
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const banners = compact([
|
|
||||||
updateMarketStateProposals.length
|
|
||||||
? {
|
|
||||||
kind: 'UpdateMarketState' as const,
|
|
||||||
proposals: updateMarketStateProposals,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
updateMarketProposals.length
|
|
||||||
? {
|
|
||||||
kind: 'UpdateMarket' as const,
|
|
||||||
proposals: updateMarketProposals,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
successorProposals.length
|
|
||||||
? {
|
|
||||||
kind: 'NewMarket' as const,
|
|
||||||
proposals: successorProposals,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
marketState === MarketState.STATE_SETTLED
|
|
||||||
? {
|
|
||||||
kind: 'Settled' as const,
|
|
||||||
market,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
marketState === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
|
||||||
? {
|
|
||||||
kind: 'Suspended' as const,
|
|
||||||
market,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
maliciousOracle !== undefined
|
|
||||||
? {
|
|
||||||
kind: 'Oracle' as const,
|
|
||||||
oracle: maliciousOracle,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return <BannerQueue banners={banners} market={market} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BannerQueue = ({
|
|
||||||
banners,
|
|
||||||
market,
|
|
||||||
}: {
|
|
||||||
banners: Banner[];
|
|
||||||
market: Market;
|
|
||||||
}) => {
|
|
||||||
// for showing banner by index
|
|
||||||
const [index, setIndex] = useState(0);
|
|
||||||
|
|
||||||
const banner = banners[index];
|
|
||||||
if (!banner) return null;
|
|
||||||
|
|
||||||
let content = null;
|
|
||||||
let intent = Intent.Primary;
|
|
||||||
|
|
||||||
switch (banner.kind) {
|
|
||||||
case 'UpdateMarket': {
|
|
||||||
content = <MarketUpdateBanner proposals={banner.proposals} />;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'UpdateMarketState': {
|
|
||||||
content = (
|
|
||||||
<MarketUpdateStateBanner proposals={banner.proposals} market={market} />
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'NewMarket': {
|
|
||||||
content = <MarketSuccessorProposalBanner proposals={banner.proposals} />;
|
|
||||||
intent = Intent.Warning;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Settled': {
|
|
||||||
content = <MarketSettledBanner market={market} />;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Suspended': {
|
|
||||||
content = <MarketSuspendedBanner />;
|
|
||||||
intent = Intent.Warning;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Oracle': {
|
|
||||||
// @ts-ignore oracle cannot be undefined
|
|
||||||
content = <MarketOracleBanner oracle={banner.oracle} />;
|
|
||||||
intent = Intent.Danger;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showCount = banners.length > 1;
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
setIndex((x) => x + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationBanner
|
|
||||||
intent={intent}
|
|
||||||
onClose={onClose}
|
|
||||||
data-testid="market-banner"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{content}
|
|
||||||
{showCount ? (
|
|
||||||
<p>
|
|
||||||
{index + 1}/{banners.length}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</NotificationBanner>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { MarketOracleBanner } from './market-oracle-banner';
|
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/markets', () => ({
|
|
||||||
...jest.requireActual('@vegaprotocol/markets'),
|
|
||||||
OracleDialog: ({ open }: { open: boolean }) => {
|
|
||||||
return <div data-testid="oracle-dialog">{open ? 'open' : 'closed'}</div>;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const oracle = {
|
|
||||||
dataSourceSpecId: 'someId',
|
|
||||||
provider: {
|
|
||||||
name: 'Test oracle',
|
|
||||||
url: 'https://zombo.com',
|
|
||||||
description_markdown:
|
|
||||||
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
|
|
||||||
oracle: {
|
|
||||||
status: 'COMPROMISED' as const,
|
|
||||||
status_reason: 'Reason here',
|
|
||||||
first_verified: '2021-01-01T00:00:00Z',
|
|
||||||
last_verified: '2022-01-01T00:00:00Z',
|
|
||||||
type: 'public_key' as const,
|
|
||||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
|
||||||
},
|
|
||||||
proofs: [
|
|
||||||
{
|
|
||||||
format: 'url' as const,
|
|
||||||
available: true,
|
|
||||||
type: 'web' as const,
|
|
||||||
url: 'https://proofweb.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: 'signed_message' as const,
|
|
||||||
available: true,
|
|
||||||
type: 'public_key' as const,
|
|
||||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
|
||||||
message: 'SOMEHEX',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
github_link:
|
|
||||||
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC.toml',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('MarketOracleBanner', () => {
|
|
||||||
it('should render successfully', async () => {
|
|
||||||
render(<MarketOracleBanner oracle={oracle} />);
|
|
||||||
|
|
||||||
expect(screen.getByTestId('oracle-dialog')).toHaveTextContent('closed');
|
|
||||||
expect(screen.getByTestId('oracle-banner-status')).toHaveTextContent(
|
|
||||||
'COMPROMISED'
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Show more' }));
|
|
||||||
expect(screen.getByTestId('oracle-dialog')).toHaveTextContent('open');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
OracleDialog,
|
|
||||||
useOracleStatuses,
|
|
||||||
type useMaliciousOracle,
|
|
||||||
} from '@vegaprotocol/markets';
|
|
||||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
export type Oracle = ReturnType<typeof useMaliciousOracle>['data'];
|
|
||||||
|
|
||||||
export const MarketOracleBanner = ({
|
|
||||||
oracle,
|
|
||||||
}: {
|
|
||||||
oracle: NonNullable<Oracle>;
|
|
||||||
}) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const oracleStatuses = useOracleStatuses();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<OracleDialog open={open} onChange={setOpen} {...oracle} />
|
|
||||||
<p>
|
|
||||||
<Trans
|
|
||||||
defaults="Oracle status for this market is <0>{{status}}</0>. {{description}} <1>Show more</1>"
|
|
||||||
components={[
|
|
||||||
<span key="oracle-status" data-testid="oracle-banner-status">
|
|
||||||
status
|
|
||||||
</span>,
|
|
||||||
<ButtonLink
|
|
||||||
key="oracle-button"
|
|
||||||
onClick={() => setOpen((x) => !x)}
|
|
||||||
data-testid="oracle-banner-dialog-trigger"
|
|
||||||
>
|
|
||||||
Show more
|
|
||||||
</ButtonLink>,
|
|
||||||
]}
|
|
||||||
values={{
|
|
||||||
status: oracle.provider.oracle.status,
|
|
||||||
description: oracleStatuses[oracle.provider.oracle.status],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { addDays, subDays, subHours } from 'date-fns';
|
|
||||||
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
|
|
||||||
import {
|
|
||||||
SuccessorMarketDocument,
|
|
||||||
MarketCandlesDocument,
|
|
||||||
MarketCandlesUpdateDocument,
|
|
||||||
SuccessorMarketIdDocument,
|
|
||||||
type Market,
|
|
||||||
type SuccessorMarketQuery,
|
|
||||||
type SuccessorMarketQueryVariables,
|
|
||||||
type MarketCandlesQuery,
|
|
||||||
type MarketCandlesQueryVariables,
|
|
||||||
type MarketCandlesUpdateSubscription,
|
|
||||||
type MarketCandlesUpdateSubscriptionVariables,
|
|
||||||
type SuccessorMarketIdQuery,
|
|
||||||
type SuccessorMarketIdQueryVariables,
|
|
||||||
} from '@vegaprotocol/markets';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
|
||||||
import { Interval, MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
|
||||||
import { MarketSettledBanner } from './market-settled-banner';
|
|
||||||
|
|
||||||
describe('MarketSettledBanner', () => {
|
|
||||||
const now = 1701388800000;
|
|
||||||
const origDateNow = Date.now;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Date.now = () => now;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
Date.now = origDateNow;
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketId = 'market-0';
|
|
||||||
const successorId = 'successor-id';
|
|
||||||
const successorName = 'successor-name';
|
|
||||||
|
|
||||||
const successorMarketIdMock: MockedResponse<
|
|
||||||
SuccessorMarketIdQuery,
|
|
||||||
SuccessorMarketIdQueryVariables
|
|
||||||
> = {
|
|
||||||
request: {
|
|
||||||
query: SuccessorMarketIdDocument,
|
|
||||||
variables: {
|
|
||||||
marketId: marketId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
market: {
|
|
||||||
successorMarketID: successorId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const successorMock: MockedResponse<
|
|
||||||
SuccessorMarketQuery,
|
|
||||||
SuccessorMarketQueryVariables
|
|
||||||
> = {
|
|
||||||
request: {
|
|
||||||
query: SuccessorMarketDocument,
|
|
||||||
variables: {
|
|
||||||
marketId: successorId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
market: {
|
|
||||||
id: successorId,
|
|
||||||
state: MarketState.STATE_ACTIVE,
|
|
||||||
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
|
||||||
positionDecimalPlaces: 0,
|
|
||||||
tradableInstrument: {
|
|
||||||
instrument: {
|
|
||||||
name: successorName,
|
|
||||||
code: 'code',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
proposal: {
|
|
||||||
id: 'proposal-id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const since = subDays(new Date(now), 5).toISOString();
|
|
||||||
const successorCandlesMock: MockedResponse<
|
|
||||||
MarketCandlesQuery,
|
|
||||||
MarketCandlesQueryVariables
|
|
||||||
> = {
|
|
||||||
request: {
|
|
||||||
query: MarketCandlesDocument,
|
|
||||||
variables: {
|
|
||||||
marketId: successorId,
|
|
||||||
interval: Interval.INTERVAL_I1H,
|
|
||||||
since,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
marketsConnection: {
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
candlesConnection: {
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
high: '100',
|
|
||||||
low: '100',
|
|
||||||
open: '100',
|
|
||||||
close: '100',
|
|
||||||
volume: '100',
|
|
||||||
periodStart: subHours(new Date(now), 1).toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
high: '100',
|
|
||||||
low: '100',
|
|
||||||
open: '100',
|
|
||||||
close: '200',
|
|
||||||
volume: '100',
|
|
||||||
periodStart: subHours(new Date(now), 2).toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const successorCandlesUpdateMock: MockedResponse<
|
|
||||||
MarketCandlesUpdateSubscription,
|
|
||||||
MarketCandlesUpdateSubscriptionVariables
|
|
||||||
> = {
|
|
||||||
request: {
|
|
||||||
query: MarketCandlesUpdateDocument,
|
|
||||||
variables: {
|
|
||||||
marketId: successorId,
|
|
||||||
interval: Interval.INTERVAL_I1H,
|
|
||||||
// @ts-ignore this query doesn't need the 'since' variable, but the data-provider
|
|
||||||
// uses all variables for both the query and the subscription so its needed here
|
|
||||||
// to match the mock
|
|
||||||
since,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
candles: {
|
|
||||||
high: '100',
|
|
||||||
low: '100',
|
|
||||||
open: '100',
|
|
||||||
close: '100',
|
|
||||||
volume: '100',
|
|
||||||
periodStart: '2020-01-01T00:00:00',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderComponent = (market: Market, mocks?: MockedResponse[]) => {
|
|
||||||
return render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<MockedProvider mocks={mocks}>
|
|
||||||
<MarketSettledBanner market={market} />
|
|
||||||
</MockedProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
it('shows that the market is settled if there is no successor', () => {
|
|
||||||
const market = createMarketFragment({
|
|
||||||
id: marketId,
|
|
||||||
successorMarketID: undefined,
|
|
||||||
});
|
|
||||||
renderComponent(market, [successorMarketIdMock, successorMock]);
|
|
||||||
expect(
|
|
||||||
screen.getByText('This market has been settled')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows that the market has been succeeded with an active market', async () => {
|
|
||||||
const market = createMarketFragment({
|
|
||||||
id: marketId,
|
|
||||||
successorMarketID: successorId,
|
|
||||||
marketTimestamps: {
|
|
||||||
close: addDays(new Date(now), 1).toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
renderComponent(market, [
|
|
||||||
successorMarketIdMock,
|
|
||||||
successorMock,
|
|
||||||
successorCandlesMock,
|
|
||||||
successorCandlesUpdateMock,
|
|
||||||
]);
|
|
||||||
expect(
|
|
||||||
await screen.findByText('This market has been succeeded')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
await screen.findByText(/has a 24h trading volume of 200$/i)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText('This market expires in 1 day.')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('link', { name: successorName })).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
`/markets/${successorId}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import { isBefore, formatDuration, intervalToDuration } from 'date-fns';
|
|
||||||
import {
|
|
||||||
calcCandleVolume,
|
|
||||||
useCandles,
|
|
||||||
useSuccessorMarket,
|
|
||||||
type Market,
|
|
||||||
} from '@vegaprotocol/markets';
|
|
||||||
import {
|
|
||||||
addDecimalsFormatNumber,
|
|
||||||
getMarketExpiryDate,
|
|
||||||
isNumeric,
|
|
||||||
} from '@vegaprotocol/utils';
|
|
||||||
import { useT, ns } from '../../lib/use-t';
|
|
||||||
import { Links } from '../../lib/links';
|
|
||||||
|
|
||||||
const getExpiryDate = (tags: string[], close?: string): Date | null => {
|
|
||||||
const expiryDate = getMarketExpiryDate(tags);
|
|
||||||
return expiryDate || (close && new Date(close)) || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MarketSettledBanner = ({ market }: { market: Market }) => {
|
|
||||||
const t = useT();
|
|
||||||
const { data: successorMarket } = useSuccessorMarket(market.id);
|
|
||||||
|
|
||||||
const expiry = market
|
|
||||||
? getExpiryDate(
|
|
||||||
market.tradableInstrument.instrument.metadata.tags || [],
|
|
||||||
market.marketTimestamps.close
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const duration =
|
|
||||||
expiry && isBefore(new Date(Date.now()), expiry)
|
|
||||||
? intervalToDuration({ start: new Date(Date.now()), end: expiry })
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { oneDayCandles } = useCandles({
|
|
||||||
marketId: successorMarket?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const candleVolume = oneDayCandles?.length
|
|
||||||
? calcCandleVolume(oneDayCandles)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const successorVolume =
|
|
||||||
candleVolume && isNumeric(successorMarket?.positionDecimalPlaces)
|
|
||||||
? addDecimalsFormatNumber(
|
|
||||||
candleVolume,
|
|
||||||
successorMarket?.positionDecimalPlaces as number
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (successorMarket) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>{t('This market has been succeeded')}</p>
|
|
||||||
{duration && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<span>
|
|
||||||
{t('This market expires in {{duration}}.', {
|
|
||||||
duration: formatDuration(duration, {
|
|
||||||
format: [
|
|
||||||
'years',
|
|
||||||
'months',
|
|
||||||
'weeks',
|
|
||||||
'days',
|
|
||||||
'hours',
|
|
||||||
'minutes',
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
{successorVolume ? (
|
|
||||||
<Trans
|
|
||||||
defaults="The successor market <0>{{instrumentName}}</0> has a 24h trading volume of {{successorVolume}}"
|
|
||||||
values={{
|
|
||||||
successorVolume,
|
|
||||||
instrumentName:
|
|
||||||
successorMarket?.tradableInstrument.instrument.name,
|
|
||||||
}}
|
|
||||||
components={[
|
|
||||||
<Link
|
|
||||||
to={Links.MARKET(successorMarket.id)}
|
|
||||||
key="link"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
successor market name
|
|
||||||
</Link>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Trans
|
|
||||||
defaults="The successor market is <0>{{instrumentName}}</0>"
|
|
||||||
values={{
|
|
||||||
instrumentName:
|
|
||||||
successorMarket?.tradableInstrument.instrument.name,
|
|
||||||
}}
|
|
||||||
components={[
|
|
||||||
<Link
|
|
||||||
to={Links.MARKET(successorMarket.id)}
|
|
||||||
key="link"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
successor market name
|
|
||||||
</Link>,
|
|
||||||
]}
|
|
||||||
ns={ns}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <p>{t('This market has been settled')}</p>;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { MockedProvider } from '@apollo/react-testing';
|
||||||
|
import { MarketSuccessorBanner } from './market-successor-banner';
|
||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
import * as allUtils from '@vegaprotocol/utils';
|
||||||
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
|
const market = {
|
||||||
|
id: 'marketId',
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
metadata: {
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marketTimestamps: {
|
||||||
|
close: null,
|
||||||
|
},
|
||||||
|
} as unknown as Market;
|
||||||
|
|
||||||
|
let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
|
||||||
|
let mockDataMarketState: Market['state'] | null = null;
|
||||||
|
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||||
|
useDataProvider: jest.fn().mockImplementation((args) => {
|
||||||
|
if (args.skip) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: mockDataSuccessorMarket,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
jest.mock('@vegaprotocol/utils', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/utils'),
|
||||||
|
getMarketExpiryDate: jest.fn(),
|
||||||
|
}));
|
||||||
|
let mockCandles = {};
|
||||||
|
jest.mock('@vegaprotocol/markets', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/markets'),
|
||||||
|
useMarketState: (marketId: string) =>
|
||||||
|
marketId
|
||||||
|
? {
|
||||||
|
data: mockDataMarketState,
|
||||||
|
}
|
||||||
|
: { data: undefined },
|
||||||
|
useSuccessorMarket: (marketId: string) =>
|
||||||
|
marketId
|
||||||
|
? {
|
||||||
|
data: mockDataSuccessorMarket,
|
||||||
|
}
|
||||||
|
: { data: undefined },
|
||||||
|
useCandles: () => mockCandles,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('MarketSuccessorBanner', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockDataSuccessorMarket = {
|
||||||
|
id: 'successorMarketID',
|
||||||
|
state: Types.MarketState.STATE_ACTIVE,
|
||||||
|
tradingMode: Types.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'Successor Market Name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
describe('should be hidden', () => {
|
||||||
|
it('when no market', () => {
|
||||||
|
const { container } = render(<MarketSuccessorBanner market={null} />, {
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
});
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no successor market data', () => {
|
||||||
|
mockDataSuccessorMarket = null;
|
||||||
|
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
});
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should be displayed', () => {
|
||||||
|
it('should be rendered', () => {
|
||||||
|
render(<MarketSuccessorBanner market={market} />, {
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByText('This market has been succeeded')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('link', { name: 'Successor Market Name' })
|
||||||
|
).toHaveAttribute('href', '/#/markets/successorMarketID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no successor market data, market settled', () => {
|
||||||
|
mockDataSuccessorMarket = null;
|
||||||
|
mockDataMarketState = Types.MarketState.STATE_SETTLED;
|
||||||
|
render(<MarketSuccessorBanner market={market} />, {
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByText('This market has been settled')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display optionally successor volume', () => {
|
||||||
|
mockDataSuccessorMarket = {
|
||||||
|
...mockDataSuccessorMarket,
|
||||||
|
positionDecimalPlaces: 3,
|
||||||
|
};
|
||||||
|
mockCandles = {
|
||||||
|
oneDayCandles: [
|
||||||
|
{ volume: 123 },
|
||||||
|
{ volume: 456 },
|
||||||
|
{ volume: 789 },
|
||||||
|
{ volume: 99999 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<MarketSuccessorBanner market={market} />, {
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByText('has a 24h trading volume of 101.367', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display optionally duration', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(allUtils, 'getMarketExpiryDate')
|
||||||
|
.mockReturnValue(
|
||||||
|
new Date(Date.now() + 24 * 60 * 60 * 1000 + 60 * 1000)
|
||||||
|
);
|
||||||
|
render(<MarketSuccessorBanner market={market} />, {
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByText(/^This market expires in 1 day/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { isBefore, formatDuration, intervalToDuration } from 'date-fns';
|
||||||
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
|
import {
|
||||||
|
calcCandleVolume,
|
||||||
|
useCandles,
|
||||||
|
useMarketState,
|
||||||
|
useSuccessorMarket,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
Intent,
|
||||||
|
NotificationBanner,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
getMarketExpiryDate,
|
||||||
|
isNumeric,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
import { useT, ns } from '../../lib/use-t';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
const getExpiryDate = (tags: string[], close?: string): Date | null => {
|
||||||
|
const expiryDate = getMarketExpiryDate(tags);
|
||||||
|
return expiryDate || (close && new Date(close)) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarketSuccessorBanner = ({
|
||||||
|
market,
|
||||||
|
}: {
|
||||||
|
market: Market | null;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const { data: marketState } = useMarketState(market?.id);
|
||||||
|
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
|
||||||
|
const { data: successorData, loading } = useSuccessorMarket(market?.id);
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
|
||||||
|
const expiry = market
|
||||||
|
? getExpiryDate(
|
||||||
|
market.tradableInstrument.instrument.metadata.tags || [],
|
||||||
|
market.marketTimestamps.close
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const duration =
|
||||||
|
expiry && isBefore(new Date(), expiry)
|
||||||
|
? intervalToDuration({ start: new Date(), end: expiry })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const { oneDayCandles } = useCandles({
|
||||||
|
marketId: successorData?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const candleVolume = oneDayCandles?.length
|
||||||
|
? calcCandleVolume(oneDayCandles)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const successorVolume =
|
||||||
|
candleVolume && isNumeric(successorData?.positionDecimalPlaces)
|
||||||
|
? addDecimalsFormatNumber(
|
||||||
|
candleVolume,
|
||||||
|
successorData?.positionDecimalPlaces as number
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!loading && (isSettled || successorData) && visible) {
|
||||||
|
return (
|
||||||
|
<NotificationBanner
|
||||||
|
intent={Intent.Primary}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="uppercase">
|
||||||
|
{successorData
|
||||||
|
? t('This market has been succeeded')
|
||||||
|
: t('This market has been settled')}
|
||||||
|
</div>
|
||||||
|
{(duration || successorData) && (
|
||||||
|
<div className="mt-1">
|
||||||
|
{duration && (
|
||||||
|
<span>
|
||||||
|
{t('This market expires in {{duration}}.', {
|
||||||
|
duration: formatDuration(duration, {
|
||||||
|
format: [
|
||||||
|
'years',
|
||||||
|
'months',
|
||||||
|
'weeks',
|
||||||
|
'days',
|
||||||
|
'hours',
|
||||||
|
'minutes',
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{successorData && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
{successorVolume ? (
|
||||||
|
<Trans
|
||||||
|
defaults="The successor market <0>{{instrumentName}}</0> has a 24h trading volume of {{successorVolume}}"
|
||||||
|
values={{
|
||||||
|
successorVolume,
|
||||||
|
instrumentName:
|
||||||
|
successorData?.tradableInstrument.instrument.name,
|
||||||
|
}}
|
||||||
|
components={[
|
||||||
|
<ExternalLink
|
||||||
|
href={`/#/markets/${successorData?.id}`}
|
||||||
|
key="link"
|
||||||
|
>
|
||||||
|
successor market name
|
||||||
|
</ExternalLink>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Trans
|
||||||
|
defaults="The successor market is <0>{{instrumentName}}</0>"
|
||||||
|
values={{
|
||||||
|
instrumentName:
|
||||||
|
successorData?.tradableInstrument.instrument.name,
|
||||||
|
}}
|
||||||
|
components={[
|
||||||
|
<ExternalLink
|
||||||
|
href={`/#/markets/${successorData?.id}`}
|
||||||
|
key="link"
|
||||||
|
>
|
||||||
|
successor market name
|
||||||
|
</ExternalLink>,
|
||||||
|
]}
|
||||||
|
ns={ns}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</NotificationBanner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,77 +1,164 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import type { SingleExecutionResult } from '@apollo/client';
|
||||||
|
import type { MockedResponse } from '@apollo/react-testing';
|
||||||
|
import { MockedProvider } from '@apollo/react-testing';
|
||||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
|
||||||
|
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
|
||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
const proposal = {
|
const marketProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||||
__typename: 'Proposal' as const,
|
request: {
|
||||||
id: 'proposal-1',
|
query: MarketViewProposalsDocument,
|
||||||
state: ProposalState.STATE_OPEN,
|
variables: {
|
||||||
terms: {
|
inState: Types.ProposalState.STATE_OPEN,
|
||||||
__typename: 'ProposalTerms' as const,
|
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||||
closingDatetime: '2023-09-27',
|
|
||||||
enactmentDatetime: '2023-09-28',
|
|
||||||
change: {
|
|
||||||
__typename: 'NewMarket' as const,
|
|
||||||
instrument: {
|
|
||||||
name: 'New proposal of the market successor',
|
|
||||||
},
|
|
||||||
successorConfiguration: {
|
|
||||||
parentMarketId: 'marketId',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
result: {
|
||||||
const proposal2 = {
|
data: {
|
||||||
__typename: 'Proposal' as const,
|
proposalsConnection: {
|
||||||
id: 'proposal-2',
|
edges: [
|
||||||
state: ProposalState.STATE_OPEN,
|
{
|
||||||
terms: {
|
node: {
|
||||||
__typename: 'ProposalTerms' as const,
|
__typename: 'Proposal',
|
||||||
closingDatetime: '2023-09-27',
|
id: 'proposal-1',
|
||||||
enactmentDatetime: '2023-09-28',
|
state: Types.ProposalState.STATE_OPEN,
|
||||||
change: {
|
terms: {
|
||||||
__typename: 'NewMarket' as const,
|
__typename: 'ProposalTerms',
|
||||||
instrument: {
|
closingDatetime: '2023-09-27',
|
||||||
name: 'New second proposal of the market successor',
|
enactmentDatetime: '2023-09-28',
|
||||||
},
|
change: {
|
||||||
successorConfiguration: {
|
__typename: 'NewMarket',
|
||||||
parentMarketId: 'marketId',
|
instrument: {
|
||||||
|
name: 'New proposal of the market successor',
|
||||||
|
},
|
||||||
|
successorConfiguration: {
|
||||||
|
parentMarketId: 'marketId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('MarketSuccessorProposalBanner', () => {
|
describe('MarketSuccessorProposalBanner', () => {
|
||||||
it('should display single proposal', () => {
|
it('should display single proposal', async () => {
|
||||||
render(<MarketSuccessorProposalBanner proposals={[proposal]} />);
|
render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
expect(
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
screen.getByText('A successor to this market has been proposed')
|
</MockedProvider>
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(screen.getByRole('link')).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
expect.stringContaining(proposal.id)
|
|
||||||
);
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('A successors to this market has been proposed')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getByRole('link')
|
||||||
|
.getAttribute('href')
|
||||||
|
?.endsWith('/proposals/proposal-1') ?? false
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it('should display plural proposals', async () => {
|
||||||
|
const dualProposalMock = {
|
||||||
|
...marketProposalMock,
|
||||||
|
result: {
|
||||||
|
...marketProposalMock.result,
|
||||||
|
data: {
|
||||||
|
proposalsConnection: {
|
||||||
|
edges: [
|
||||||
|
...((
|
||||||
|
marketProposalMock?.result as SingleExecutionResult<MarketViewProposalsQuery>
|
||||||
|
)?.data?.proposalsConnection?.edges ?? []),
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
__typename: 'Proposal',
|
||||||
|
id: 'proposal-2',
|
||||||
|
state: Types.ProposalState.STATE_OPEN,
|
||||||
|
terms: {
|
||||||
|
__typename: 'ProposalTerms',
|
||||||
|
change: {
|
||||||
|
__typename: 'NewMarket',
|
||||||
|
instrument: {
|
||||||
|
name: 'New second proposal of the market successor',
|
||||||
|
},
|
||||||
|
successorConfiguration: {
|
||||||
|
parentMarketId: 'marketId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[dualProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('Successors to this market have been proposed')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getAllByRole('link')[0]
|
||||||
|
.getAttribute('href')
|
||||||
|
?.endsWith('/proposals/proposal-1') ?? false
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getAllByRole('link')[1]
|
||||||
|
.getAttribute('href')
|
||||||
|
?.endsWith('/proposals/proposal-2') ?? false
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display plural proposals', () => {
|
it('banner should be hidden because no proposals', () => {
|
||||||
const proposals = [proposal, proposal2];
|
const { container } = render(
|
||||||
render(<MarketSuccessorProposalBanner proposals={proposals} />);
|
<MockedProvider>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
expect(
|
</MockedProvider>
|
||||||
screen.getByText('Successors to this market have been proposed')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
const links = screen.getAllByRole('link');
|
|
||||||
expect(links).toHaveLength(proposals.length);
|
|
||||||
expect(links[0]).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
expect.stringContaining(proposal.id)
|
|
||||||
);
|
);
|
||||||
expect(links[1]).toHaveAttribute(
|
expect(container).toBeEmptyDOMElement();
|
||||||
'href',
|
});
|
||||||
expect.stringContaining(proposal2.id)
|
|
||||||
|
it('banner should be hidden because no proposals for the market', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="otherMarketId" />
|
||||||
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('banner should be hidden after user close click', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('A successors to this market has been proposed')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await act(() => {
|
||||||
|
screen.getByTestId('notification-banner-close').click();
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,51 +1,89 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
import {
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
marketViewProposalsDataProvider,
|
||||||
|
type NewMarketSuccessorFieldsFragment,
|
||||||
|
} from '@vegaprotocol/proposals';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
Intent,
|
||||||
|
NotificationBanner,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
export const MarketSuccessorProposalBanner = ({
|
export const MarketSuccessorProposalBanner = ({
|
||||||
proposals,
|
marketId,
|
||||||
}: {
|
}: {
|
||||||
proposals: MarketViewProposalFieldsFragment[];
|
marketId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
|
const { data: proposals } = useDataProvider({
|
||||||
|
dataProvider: marketViewProposalsDataProvider,
|
||||||
|
skip: !marketId,
|
||||||
|
variables: {
|
||||||
|
inState: Types.ProposalState.STATE_OPEN,
|
||||||
|
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const successors =
|
||||||
|
proposals?.filter((item) => {
|
||||||
|
if (item.terms.change.__typename === 'NewMarket') {
|
||||||
|
const newMarket = item.terms.change;
|
||||||
|
if (
|
||||||
|
newMarket.successorConfiguration?.parentMarketId === marketId &&
|
||||||
|
item.state === Types.ProposalState.STATE_OPEN
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}) ?? [];
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
const tokenLink = useLinks(DApp.Governance);
|
const tokenLink = useLinks(DApp.Governance);
|
||||||
|
if (visible && successors.length) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<NotificationBanner
|
||||||
<div className="uppercase mb-1">
|
intent={Intent.Primary}
|
||||||
{proposals.length === 1
|
onClose={() => {
|
||||||
? t('A successor to this market has been proposed')
|
setVisible(false);
|
||||||
: t('Successors to this market have been proposed')}
|
}}
|
||||||
</div>
|
>
|
||||||
<div>
|
<div className="uppercase mb-1">
|
||||||
{t(
|
{successors.length === 1
|
||||||
'checkOutProposalsAndVote',
|
? t('A successors to this market has been proposed')
|
||||||
'Check out the terms of the proposals and vote:',
|
: t('Successors to this market have been proposed')}
|
||||||
{
|
</div>
|
||||||
count: proposals.length,
|
<div>
|
||||||
}
|
{t(
|
||||||
)}{' '}
|
'checkOutProposalsAndVote',
|
||||||
{proposals.map((item, i) => {
|
'Check out the terms of the proposals and vote:',
|
||||||
if (item.terms.change.__typename !== 'NewMarket') {
|
{
|
||||||
return null;
|
count: successors.length,
|
||||||
}
|
}
|
||||||
|
)}{' '}
|
||||||
const externalLink = tokenLink(
|
{successors.map((item, i) => {
|
||||||
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
const externalLink = tokenLink(
|
||||||
);
|
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
<ExternalLink href={externalLink} key={i}>
|
<ExternalLink href={externalLink} key={i}>
|
||||||
{item.terms.change.instrument.name}
|
{
|
||||||
</ExternalLink>
|
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||||
{i < proposals.length - 1 && ', '}
|
?.instrument.name
|
||||||
</Fragment>
|
}
|
||||||
);
|
</ExternalLink>
|
||||||
})}
|
{i < successors.length - 1 && ', '}
|
||||||
</div>
|
</Fragment>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
|
</div>
|
||||||
|
</NotificationBanner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { useT } from '../../lib/use-t';
|
|
||||||
|
|
||||||
export const MarketSuspendedBanner = () => {
|
|
||||||
const t = useT();
|
|
||||||
return <p>{t('Market was suspended by governance')}</p>;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
|
||||||
|
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
|
||||||
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
|
import { MarketTerminationBanner } from './market-termination-banner';
|
||||||
|
|
||||||
|
const marketMock = {
|
||||||
|
id: 'market-1',
|
||||||
|
decimalPlaces: 3,
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
product: {
|
||||||
|
__typename: 'Future',
|
||||||
|
quoteName: 'tDAI',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Market;
|
||||||
|
|
||||||
|
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||||
|
request: {
|
||||||
|
query: MarketViewProposalsDocument,
|
||||||
|
variables: {
|
||||||
|
inState: Types.ProposalState.STATE_PASSED,
|
||||||
|
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
proposalsConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '1',
|
||||||
|
state: Types.ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: '2023-09-27T11:48:18Z',
|
||||||
|
enactmentDatetime: '2023-09-30T11:48:18',
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarketState',
|
||||||
|
updateType:
|
||||||
|
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||||
|
price: '',
|
||||||
|
market: {
|
||||||
|
id: 'market-1',
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'Market one name',
|
||||||
|
code: 'Market one',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '2',
|
||||||
|
state: Types.ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: '2023-09-27T11:48:18Z',
|
||||||
|
enactmentDatetime: '2023-10-01T11:48:18',
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarketState',
|
||||||
|
updateType:
|
||||||
|
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||||
|
price: '',
|
||||||
|
market: {
|
||||||
|
id: 'market-2',
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'Market two name',
|
||||||
|
code: 'Market two',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const openProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||||
|
request: {
|
||||||
|
query: MarketViewProposalsDocument,
|
||||||
|
variables: {
|
||||||
|
inState: Types.ProposalState.STATE_OPEN,
|
||||||
|
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
proposalsConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '3',
|
||||||
|
state: Types.ProposalState.STATE_OPEN,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: '2023-09-27T11:48:18Z',
|
||||||
|
enactmentDatetime: '2023-10-01T11:48:18',
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarketState',
|
||||||
|
updateType:
|
||||||
|
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||||
|
price: '',
|
||||||
|
market: {
|
||||||
|
id: 'market-3',
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'Market three name',
|
||||||
|
code: 'Market three',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '4',
|
||||||
|
state: Types.ProposalState.STATE_OPEN,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: '2023-09-27T11:48:18Z',
|
||||||
|
enactmentDatetime: '2023-10-11T11:48:18',
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarketState',
|
||||||
|
updateType:
|
||||||
|
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||||
|
price: '',
|
||||||
|
market: {
|
||||||
|
id: 'market-3',
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'Market three name',
|
||||||
|
code: 'Market three',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '5',
|
||||||
|
state: Types.ProposalState.STATE_OPEN,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: '2023-09-27T11:48:18Z',
|
||||||
|
enactmentDatetime: '2023-10-01T11:48:18',
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarketState',
|
||||||
|
updateType:
|
||||||
|
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||||
|
price: '',
|
||||||
|
market: {
|
||||||
|
id: 'market-4',
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'Market four name',
|
||||||
|
code: 'Market four',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mocks: MockedResponse[] = [passedProposalMock, openProposalMock];
|
||||||
|
|
||||||
|
describe('MarketTerminationBanner', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers().setSystemTime(new Date('2023-09-28T10:10:10.000Z'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be properly rendered', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<MarketTerminationBanner market={marketMock} />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container).not.toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('termination-warning-banner-market-1')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render link to proposals', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<MarketTerminationBanner market={{ ...marketMock, id: 'market-3' }} />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container).not.toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('View proposals')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render link to proposal', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<MarketTerminationBanner market={{ ...marketMock, id: 'market-4' }} />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container).not.toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('View proposal')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
+154
-107
@@ -1,120 +1,44 @@
|
|||||||
import { type ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import sortBy from 'lodash/sortBy';
|
import { useState } from 'react';
|
||||||
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
||||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
import {
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
ExternalLink,
|
||||||
|
Intent,
|
||||||
|
NotificationBanner,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||||
|
import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals';
|
||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
|
import { getQuoteName } from '@vegaprotocol/markets';
|
||||||
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
import {
|
import {
|
||||||
DApp,
|
DApp,
|
||||||
TOKEN_PROPOSAL,
|
TOKEN_PROPOSAL,
|
||||||
TOKEN_PROPOSALS,
|
TOKEN_PROPOSALS,
|
||||||
useLinks,
|
useLinks,
|
||||||
} from '@vegaprotocol/environment';
|
} from '@vegaprotocol/environment';
|
||||||
import { getQuoteName, type Market } from '@vegaprotocol/markets';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
export const MarketUpdateStateBanner = ({
|
const filterProposals = (
|
||||||
market,
|
data: MarketViewProposalFieldsFragment[] | null,
|
||||||
proposals,
|
marketId: string,
|
||||||
}: {
|
now: number
|
||||||
market: Market;
|
) =>
|
||||||
proposals: MarketViewProposalFieldsFragment[];
|
sortBy(
|
||||||
}) => {
|
(data || []).filter(
|
||||||
const t = useT();
|
(item) =>
|
||||||
const governanceLink = useLinks(DApp.Governance);
|
item.terms.change.__typename === 'UpdateMarketState' &&
|
||||||
|
item.terms.change.market.id === marketId &&
|
||||||
const openProposals = sortBy(
|
item.terms.change.updateType ===
|
||||||
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
|
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
|
||||||
(p) => p.terms.enactmentDatetime
|
item.terms.enactmentDatetime &&
|
||||||
|
new Date(item.terms.enactmentDatetime).getTime() > now
|
||||||
|
),
|
||||||
|
(item) => item.terms.enactmentDatetime
|
||||||
);
|
);
|
||||||
const passedProposals = sortBy(
|
|
||||||
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
|
|
||||||
(p) => p.terms.enactmentDatetime
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!passedProposals.length && !openProposals.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = market.tradableInstrument.instrument.code;
|
|
||||||
const assetSymbol = getQuoteName(market);
|
|
||||||
|
|
||||||
const proposalLink =
|
|
||||||
!passedProposals.length && openProposals[0]?.id
|
|
||||||
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const proposalsLink =
|
|
||||||
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
|
|
||||||
|
|
||||||
let content: ReactNode;
|
|
||||||
|
|
||||||
if (passedProposals.length) {
|
|
||||||
const { date, duration, price } = getMessageVariables(passedProposals[0]);
|
|
||||||
content = (
|
|
||||||
<>
|
|
||||||
<p className="uppercase mb-1">
|
|
||||||
{t('Trading on market {{name}} will stop on {{date}}', {
|
|
||||||
name,
|
|
||||||
date,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'You will no longer be able to hold a position on this market when it closes in {{duration}}.',
|
|
||||||
{ duration }
|
|
||||||
)}{' '}
|
|
||||||
{price &&
|
|
||||||
assetSymbol &&
|
|
||||||
t('The final price will be {{price}} {{assetSymbol}}.', {
|
|
||||||
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
|
||||||
assetSymbol,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (openProposals.length > 1) {
|
|
||||||
content = (
|
|
||||||
<>
|
|
||||||
<p className="uppercase mb-1">
|
|
||||||
{t(
|
|
||||||
'Trading on market {{name}} may stop. There are open proposals to close this market',
|
|
||||||
{ name }
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<ExternalLink href={proposalsLink}>
|
|
||||||
{t('View proposals')}
|
|
||||||
</ExternalLink>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const { date, price } = getMessageVariables(openProposals[0]);
|
|
||||||
content = (
|
|
||||||
<>
|
|
||||||
<p className="mb-1">
|
|
||||||
{t(
|
|
||||||
'Trading on market {{name}} may stop on {{date}}. There is an open proposal to close this market.',
|
|
||||||
{ name, date }
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{price &&
|
|
||||||
assetSymbol &&
|
|
||||||
t('Proposed final price is {{price}} {{assetSymbol}}.', {
|
|
||||||
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
|
||||||
assetSymbol,
|
|
||||||
})}{' '}
|
|
||||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div data-testid={`update-state-banner-${market.id}`}>{content}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
||||||
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
|
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
|
||||||
@@ -138,3 +62,126 @@ const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
|||||||
price,
|
price,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MarketTerminationBanner = ({
|
||||||
|
market,
|
||||||
|
}: {
|
||||||
|
market: Market | null;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
const skip = !market || !visible;
|
||||||
|
const { data: passedProposalsData } = useDataProvider({
|
||||||
|
dataProvider: marketViewProposalsDataProvider,
|
||||||
|
skip,
|
||||||
|
variables: {
|
||||||
|
inState: Types.ProposalState.STATE_PASSED,
|
||||||
|
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: openProposalsData } = useDataProvider({
|
||||||
|
dataProvider: marketViewProposalsDataProvider,
|
||||||
|
skip,
|
||||||
|
variables: {
|
||||||
|
inState: Types.ProposalState.STATE_OPEN,
|
||||||
|
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const governanceLink = useLinks(DApp.Governance);
|
||||||
|
|
||||||
|
if (!market) return null;
|
||||||
|
const now = Date.now();
|
||||||
|
const passedProposals = filterProposals(passedProposalsData, market.id, now);
|
||||||
|
const openProposals = filterProposals(openProposalsData, market.id, now);
|
||||||
|
|
||||||
|
const name = market.tradableInstrument.instrument.code;
|
||||||
|
if (!passedProposals.length && !openProposals.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetSymbol = getQuoteName(market);
|
||||||
|
const proposalLink =
|
||||||
|
!passedProposals.length && openProposals[0]?.id
|
||||||
|
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
|
||||||
|
: undefined;
|
||||||
|
const proposalsLink =
|
||||||
|
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
|
||||||
|
let content: ReactNode;
|
||||||
|
if (passedProposals.length) {
|
||||||
|
const { date, duration, price } = getMessageVariables(passedProposals[0]);
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
<div className="uppercase mb-1">
|
||||||
|
{t('Trading on Market {{name}} will stop on {{date}}', {
|
||||||
|
name,
|
||||||
|
date,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t(
|
||||||
|
'You will no longer be able to hold a position on this market when it closes in {{duration}}.',
|
||||||
|
{ duration }
|
||||||
|
)}{' '}
|
||||||
|
{price &&
|
||||||
|
assetSymbol &&
|
||||||
|
t('The final price will be {{price}} {{assetSymbol}}.', {
|
||||||
|
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||||
|
assetSymbol,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (openProposals.length > 1) {
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
<div className="uppercase mb-1">
|
||||||
|
{t(
|
||||||
|
'Trading on Market {{name}} may stop. There are open proposals to close this market',
|
||||||
|
{ name }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ExternalLink href={proposalsLink}>
|
||||||
|
{t('View proposals')}
|
||||||
|
</ExternalLink>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { date, price } = getMessageVariables(openProposals[0]);
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
<div className="uppercase mb-1">
|
||||||
|
{t(
|
||||||
|
'Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.',
|
||||||
|
{ name, date }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{price &&
|
||||||
|
assetSymbol &&
|
||||||
|
t('Proposed final price is {{price}} {{assetSymbol}}.', {
|
||||||
|
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||||
|
assetSymbol,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<NotificationBanner
|
||||||
|
intent={openProposals.length ? Intent.Warning : Intent.Info}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
data-testid={`termination-warning-banner-${market.id}`}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</NotificationBanner>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
|
||||||
import { MarketUpdateBanner } from './market-update-banner';
|
|
||||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
|
||||||
|
|
||||||
describe('MarketUpdateBanner', () => {
|
|
||||||
const change = {
|
|
||||||
__typename: 'UpdateMarket' as const,
|
|
||||||
marketId: 'update-market-id',
|
|
||||||
};
|
|
||||||
|
|
||||||
const openProposal = {
|
|
||||||
__typename: 'Proposal' as const,
|
|
||||||
id: 'proposal-1',
|
|
||||||
state: ProposalState.STATE_OPEN,
|
|
||||||
terms: {
|
|
||||||
__typename: 'ProposalTerms' as const,
|
|
||||||
closingDatetime: '2023-09-27',
|
|
||||||
enactmentDatetime: '2023-09-28',
|
|
||||||
change,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const passedProposal = {
|
|
||||||
__typename: 'Proposal' as const,
|
|
||||||
id: 'proposal-2',
|
|
||||||
state: ProposalState.STATE_PASSED,
|
|
||||||
terms: {
|
|
||||||
__typename: 'ProposalTerms' as const,
|
|
||||||
closingDatetime: '2023-09-27',
|
|
||||||
enactmentDatetime: '2023-09-28',
|
|
||||||
change,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
it('renders content for a single open proposal', () => {
|
|
||||||
render(
|
|
||||||
<MarketUpdateBanner
|
|
||||||
proposals={[openProposal as MarketViewProposalFieldsFragment]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(/^changes have been proposed/i)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('link', { name: 'View proposal' })).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
expect.stringContaining(openProposal.id as string)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders content for a single passed proposal', () => {
|
|
||||||
render(
|
|
||||||
<MarketUpdateBanner
|
|
||||||
proposals={[passedProposal as MarketViewProposalFieldsFragment]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(/^proposal set to change market/i)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('link', { name: 'View proposal' })).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
expect.stringContaining(passedProposal.id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders content for multiple passed proposals', () => {
|
|
||||||
const proposals = [
|
|
||||||
openProposal,
|
|
||||||
openProposal,
|
|
||||||
] as MarketViewProposalFieldsFragment[];
|
|
||||||
|
|
||||||
render(<MarketUpdateBanner proposals={proposals} />);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(
|
|
||||||
new RegExp(`^There are ${proposals.length} open proposals`)
|
|
||||||
)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByRole('link', { name: 'View proposals' })
|
|
||||||
).toHaveAttribute('href', expect.stringContaining('proposals'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import {
|
|
||||||
DApp,
|
|
||||||
TOKEN_PROPOSAL,
|
|
||||||
TOKEN_PROPOSALS,
|
|
||||||
useLinks,
|
|
||||||
} from '@vegaprotocol/environment';
|
|
||||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
|
||||||
import { useT } from '../../lib/use-t';
|
|
||||||
|
|
||||||
export const MarketUpdateBanner = ({
|
|
||||||
proposals,
|
|
||||||
}: {
|
|
||||||
proposals: MarketViewProposalFieldsFragment[];
|
|
||||||
}) => {
|
|
||||||
const governanceLink = useLinks(DApp.Governance);
|
|
||||||
const t = useT();
|
|
||||||
const openProposals = sortBy(
|
|
||||||
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
|
|
||||||
(p) => p.terms.enactmentDatetime
|
|
||||||
);
|
|
||||||
const passedProposals = sortBy(
|
|
||||||
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
|
|
||||||
(p) => p.terms.enactmentDatetime
|
|
||||||
);
|
|
||||||
|
|
||||||
let content = null;
|
|
||||||
|
|
||||||
if (openProposals.length > 1) {
|
|
||||||
content = (
|
|
||||||
<p>
|
|
||||||
{t('There are {{count}} open proposals to change this market', {
|
|
||||||
count: openProposals.length,
|
|
||||||
})}
|
|
||||||
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)}>
|
|
||||||
{t('View proposals')}
|
|
||||||
</ExternalLink>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
} else if (passedProposals.length) {
|
|
||||||
const proposal = passedProposals[0];
|
|
||||||
const proposalLink = governanceLink(
|
|
||||||
TOKEN_PROPOSAL.replace(':id', proposal?.id || '')
|
|
||||||
);
|
|
||||||
|
|
||||||
content = (
|
|
||||||
<p>
|
|
||||||
{t('Proposal set to change market on {{date}}.', {
|
|
||||||
date: format(new Date(proposal.terms.enactmentDatetime), 'dd MMMM'),
|
|
||||||
})}
|
|
||||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>,
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const proposal = openProposals[0];
|
|
||||||
const proposalLink = governanceLink(
|
|
||||||
TOKEN_PROPOSAL.replace(':id', proposal?.id || '')
|
|
||||||
);
|
|
||||||
content = (
|
|
||||||
<p>
|
|
||||||
{t('Changes have been proposed for this market.')}{' '}
|
|
||||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div data-testid="market-proposal-notification">{content}</div>;
|
|
||||||
};
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import {
|
|
||||||
useMarketProposals,
|
|
||||||
type MarketViewProposalFieldsFragment,
|
|
||||||
} from '@vegaprotocol/proposals';
|
|
||||||
import { ProposalState, ProposalType } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
const isPending = (p: MarketViewProposalFieldsFragment) => {
|
|
||||||
return [ProposalState.STATE_OPEN, ProposalState.STATE_PASSED].includes(
|
|
||||||
p.state
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateMarketStateProposals = (
|
|
||||||
marketId: string,
|
|
||||||
inState?: ProposalState
|
|
||||||
) => {
|
|
||||||
const { data, error, loading } = useMarketProposals({
|
|
||||||
proposalType: ProposalType.TYPE_UPDATE_MARKET_STATE,
|
|
||||||
inState,
|
|
||||||
});
|
|
||||||
|
|
||||||
const proposals = data
|
|
||||||
? data.filter(isPending).filter((p) => {
|
|
||||||
const change = p.terms.change;
|
|
||||||
|
|
||||||
if (
|
|
||||||
change.__typename === 'UpdateMarketState' &&
|
|
||||||
change.market.id === marketId
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return { data, error, loading, proposals };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateMarketProposals = (
|
|
||||||
marketId: string,
|
|
||||||
inState?: ProposalState
|
|
||||||
) => {
|
|
||||||
const { data, error, loading } = useMarketProposals({
|
|
||||||
proposalType: ProposalType.TYPE_UPDATE_MARKET,
|
|
||||||
inState,
|
|
||||||
});
|
|
||||||
|
|
||||||
const proposals = data
|
|
||||||
? data.filter(isPending).filter((p) => {
|
|
||||||
const change = p.terms.change;
|
|
||||||
if (
|
|
||||||
change.__typename === 'UpdateMarket' &&
|
|
||||||
change.marketId === marketId
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return { data, error, loading, proposals };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSuccessorMarketProposals = (
|
|
||||||
marketId: string,
|
|
||||||
inState?: ProposalState
|
|
||||||
) => {
|
|
||||||
const { data, loading, error } = useMarketProposals({
|
|
||||||
proposalType: ProposalType.TYPE_NEW_MARKET,
|
|
||||||
inState,
|
|
||||||
});
|
|
||||||
|
|
||||||
const proposals = data
|
|
||||||
? data.filter(isPending).filter((p) => {
|
|
||||||
const change = p.terms.change;
|
|
||||||
if (
|
|
||||||
change.__typename === 'NewMarket' &&
|
|
||||||
change.successorConfiguration?.parentMarketId === marketId
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return { data, error, loading, proposals };
|
|
||||||
};
|
|
||||||
@@ -97,9 +97,9 @@ const MarketData = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-2/6" role="gridcell">
|
<div className="w-2/5" role="gridcell">
|
||||||
<h3 className="flex items-baseline">
|
<h3 className="flex items-baseline">
|
||||||
<span className="overflow-hidden text-xs md:text-sm lg:text-base text-ellipsis whitespace-nowrap">
|
<span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
|
||||||
{market.tradableInstrument.instrument.code}
|
{market.tradableInstrument.instrument.code}
|
||||||
</span>
|
</span>
|
||||||
{allProducts && productType && (
|
{allProducts && productType && (
|
||||||
@@ -113,7 +113,7 @@ const MarketData = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-2/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
|
className="w-1/5 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis"
|
||||||
title={symbol}
|
title={symbol}
|
||||||
data-testid="market-selector-price"
|
data-testid="market-selector-price"
|
||||||
role="gridcell"
|
role="gridcell"
|
||||||
@@ -121,14 +121,14 @@ const MarketData = ({
|
|||||||
{price} {symbol}
|
{price} {symbol}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-2/6 sm:w-1/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
|
className="w-1/5 overflow-hidden text-xs text-right lg:text-sm whitespace-nowrap text-ellipsis"
|
||||||
title={t('24h vol')}
|
title={t('24h vol')}
|
||||||
data-testid="market-selector-volume"
|
data-testid="market-selector-volume"
|
||||||
role="gridcell"
|
role="gridcell"
|
||||||
>
|
>
|
||||||
{volume}
|
{volume}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:w-1/6 sm:flex justify-end" role="gridcell">
|
<div className="flex justify-end w-1/5" role="gridcell">
|
||||||
{oneDayCandles && (
|
{oneDayCandles && (
|
||||||
<Sparkline
|
<Sparkline
|
||||||
width={64}
|
width={64}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const MarketSelector = ({
|
|||||||
setFilter((curr) => ({ ...curr, product }));
|
setFilter((curr) => ({ ...curr, product }));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="text-sm flex sm:grid grid-cols-[2fr_1fr_1fr] gap-1 ">
|
<div className="text-sm grid grid-cols-[2fr_1fr_1fr] gap-1 ">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<TradingInput
|
<TradingInput
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -182,16 +182,16 @@ const MarketList = ({
|
|||||||
'p-2 mx-2 border-b border-default text-xs text-secondary'
|
'p-2 mx-2 border-b border-default text-xs text-secondary'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="w-2/6" role="columnheader">
|
<div className="w-2/5" role="columnheader">
|
||||||
{t('Name')}
|
{t('Name')}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/6 text-right pr-4" role="columnheader">
|
<div className="w-1/5" role="columnheader">
|
||||||
{t('Price')}
|
{t('Price')}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/6 sm:w-1/6 text-right" role="columnheader">
|
<div className="w-1/5 text-right" role="columnheader">
|
||||||
{t('24h volume')}
|
{t('24h volume')}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:w-1/6" role="columnheader" />
|
<div className="w-1/5" role="columnheader" />
|
||||||
</div>
|
</div>
|
||||||
<div ref={listRef}>
|
<div ref={listRef}>
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useParams } from 'react-router-dom';
|
|||||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is only rendered for the mobile navigation
|
* This is only rendered for the mobile navigation
|
||||||
@@ -31,16 +30,7 @@ export const NavHeader = () => {
|
|||||||
trigger={
|
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">
|
<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')}
|
{data ? data.tradableInstrument.instrument.code : t('Select market')}
|
||||||
<span
|
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
||||||
className={classNames(
|
|
||||||
'transition-transform ease-in-out duration-300',
|
|
||||||
{
|
|
||||||
'rotate-180': open,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
|
||||||
</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -162,8 +162,6 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
|||||||
const envNameMapping = useEnvNameMapping();
|
const envNameMapping = useEnvNameMapping();
|
||||||
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
|
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||||
const marketId = useGlobalStore((store) => store.marketId);
|
const marketId = useGlobalStore((store) => store.marketId);
|
||||||
const GOVERNANCE_LINK = useLinks(DApp.Governance)();
|
|
||||||
const EXPLORER_LINK = useLinks(DApp.Explorer)();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gap-3 lg:flex lg:h-full">
|
<div className="gap-3 lg:flex lg:h-full">
|
||||||
@@ -222,7 +220,7 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
|||||||
</NavbarLink>
|
</NavbarLink>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<NavbarLinkExternal to={GOVERNANCE_LINK}>
|
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
|
||||||
{t('Governance')}
|
{t('Governance')}
|
||||||
</NavbarLinkExternal>
|
</NavbarLinkExternal>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
@@ -230,13 +228,6 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
|||||||
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
|
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
|
||||||
<NavbarContent data-testid="navbar-content-resources">
|
<NavbarContent data-testid="navbar-content-resources">
|
||||||
<ul className="lg:p-4">
|
<ul className="lg:p-4">
|
||||||
{EXPLORER_LINK && (
|
|
||||||
<NavbarSubItem>
|
|
||||||
<NavbarLinkExternal to={EXPLORER_LINK}>
|
|
||||||
{t('Explorer')}
|
|
||||||
</NavbarLinkExternal>
|
|
||||||
</NavbarSubItem>
|
|
||||||
)}
|
|
||||||
{DocsLinks?.NEW_TO_VEGA && (
|
{DocsLinks?.NEW_TO_VEGA && (
|
||||||
<NavbarSubItem>
|
<NavbarSubItem>
|
||||||
<NavbarLinkExternal to={DocsLinks?.NEW_TO_VEGA}>
|
<NavbarLinkExternal to={DocsLinks?.NEW_TO_VEGA}>
|
||||||
|
|||||||
@@ -2,19 +2,14 @@ query RewardsPage($partyId: ID!) {
|
|||||||
party(id: $partyId) {
|
party(id: $partyId) {
|
||||||
id
|
id
|
||||||
vestingStats {
|
vestingStats {
|
||||||
|
# AKA hoarder reward multiplier
|
||||||
rewardBonusMultiplier
|
rewardBonusMultiplier
|
||||||
quantumBalance
|
|
||||||
epochSeq
|
|
||||||
}
|
}
|
||||||
activityStreak {
|
activityStreak {
|
||||||
activeFor
|
# vesting multiplier
|
||||||
isActive
|
|
||||||
inactiveFor
|
|
||||||
rewardDistributionMultiplier
|
|
||||||
rewardVestingMultiplier
|
rewardVestingMultiplier
|
||||||
epoch
|
# AKA streak multiplier
|
||||||
tradedVolume
|
rewardDistributionMultiplier
|
||||||
openVolume
|
|
||||||
}
|
}
|
||||||
vestingBalancesSummary {
|
vestingBalancesSummary {
|
||||||
epoch
|
epoch
|
||||||
@@ -41,74 +36,6 @@ query RewardsPage($partyId: ID!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query ActiveRewards(
|
|
||||||
$isReward: Boolean
|
|
||||||
$partyId: ID
|
|
||||||
$direction: TransferDirection
|
|
||||||
$pagination: Pagination
|
|
||||||
) {
|
|
||||||
transfersConnection(
|
|
||||||
partyId: $partyId
|
|
||||||
isReward: $isReward
|
|
||||||
direction: $direction
|
|
||||||
pagination: $pagination
|
|
||||||
) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
transfer {
|
|
||||||
amount
|
|
||||||
id
|
|
||||||
from
|
|
||||||
fromAccountType
|
|
||||||
to
|
|
||||||
toAccountType
|
|
||||||
asset {
|
|
||||||
id
|
|
||||||
symbol
|
|
||||||
decimals
|
|
||||||
name
|
|
||||||
quantum
|
|
||||||
status
|
|
||||||
}
|
|
||||||
reference
|
|
||||||
status
|
|
||||||
timestamp
|
|
||||||
kind {
|
|
||||||
... on RecurringTransfer {
|
|
||||||
startEpoch
|
|
||||||
endEpoch
|
|
||||||
dispatchStrategy {
|
|
||||||
dispatchMetric
|
|
||||||
dispatchMetricAssetId
|
|
||||||
marketIdsInScope
|
|
||||||
entityScope
|
|
||||||
individualScope
|
|
||||||
teamScope
|
|
||||||
nTopPerformers
|
|
||||||
stakingRequirement
|
|
||||||
notionalTimeWeightedAveragePositionRequirement
|
|
||||||
windowLength
|
|
||||||
lockPeriod
|
|
||||||
distributionStrategy
|
|
||||||
rankTable {
|
|
||||||
startRank
|
|
||||||
shareRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reason
|
|
||||||
}
|
|
||||||
fees {
|
|
||||||
transferId
|
|
||||||
amount
|
|
||||||
epoch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query RewardsHistory(
|
query RewardsHistory(
|
||||||
$partyId: ID!
|
$partyId: ID!
|
||||||
$epochRewardSummariesPagination: Pagination
|
$epochRewardSummariesPagination: Pagination
|
||||||
@@ -165,18 +92,3 @@ query RewardsEpoch {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query MarketForRewards($marketId: ID!) {
|
|
||||||
market(id: $marketId) {
|
|
||||||
tradableInstrument {
|
|
||||||
instrument {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
code
|
|
||||||
metadata {
|
|
||||||
tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-167
@@ -8,17 +8,7 @@ export type RewardsPageQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string, quantumBalance: string, epochSeq: number } | null, activityStreak?: { __typename?: 'PartyActivityStreak', activeFor: number, isActive: boolean, inactiveFor: number, rewardDistributionMultiplier: string, rewardVestingMultiplier: string, epoch: number, tradedVolume: string, openVolume: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
|
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string } | null, activityStreak?: { __typename?: 'PartyActivityStreak', rewardVestingMultiplier: string, rewardDistributionMultiplier: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
|
||||||
|
|
||||||
export type ActiveRewardsQueryVariables = Types.Exact<{
|
|
||||||
isReward?: Types.InputMaybe<Types.Scalars['Boolean']>;
|
|
||||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
|
||||||
direction?: Types.InputMaybe<Types.TransferDirection>;
|
|
||||||
pagination?: Types.InputMaybe<Types.Pagination>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
|
|
||||||
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
|
|
||||||
|
|
||||||
export type RewardsHistoryQueryVariables = Types.Exact<{
|
export type RewardsHistoryQueryVariables = Types.Exact<{
|
||||||
partyId: Types.Scalars['ID'];
|
partyId: Types.Scalars['ID'];
|
||||||
@@ -36,13 +26,6 @@ export type RewardsEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
|||||||
|
|
||||||
export type RewardsEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string } };
|
export type RewardsEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string } };
|
||||||
|
|
||||||
export type MarketForRewardsQueryVariables = Types.Exact<{
|
|
||||||
marketId: Types.Scalars['ID'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
|
|
||||||
export type MarketForRewardsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null } } } } | null };
|
|
||||||
|
|
||||||
|
|
||||||
export const RewardsPageDocument = gql`
|
export const RewardsPageDocument = gql`
|
||||||
query RewardsPage($partyId: ID!) {
|
query RewardsPage($partyId: ID!) {
|
||||||
@@ -50,18 +33,10 @@ export const RewardsPageDocument = gql`
|
|||||||
id
|
id
|
||||||
vestingStats {
|
vestingStats {
|
||||||
rewardBonusMultiplier
|
rewardBonusMultiplier
|
||||||
quantumBalance
|
|
||||||
epochSeq
|
|
||||||
}
|
}
|
||||||
activityStreak {
|
activityStreak {
|
||||||
activeFor
|
|
||||||
isActive
|
|
||||||
inactiveFor
|
|
||||||
rewardDistributionMultiplier
|
|
||||||
rewardVestingMultiplier
|
rewardVestingMultiplier
|
||||||
epoch
|
rewardDistributionMultiplier
|
||||||
tradedVolume
|
|
||||||
openVolume
|
|
||||||
}
|
}
|
||||||
vestingBalancesSummary {
|
vestingBalancesSummary {
|
||||||
epoch
|
epoch
|
||||||
@@ -116,101 +91,6 @@ export function useRewardsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOption
|
|||||||
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
|
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
|
||||||
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
|
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
|
||||||
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
|
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
|
||||||
export const ActiveRewardsDocument = gql`
|
|
||||||
query ActiveRewards($isReward: Boolean, $partyId: ID, $direction: TransferDirection, $pagination: Pagination) {
|
|
||||||
transfersConnection(
|
|
||||||
partyId: $partyId
|
|
||||||
isReward: $isReward
|
|
||||||
direction: $direction
|
|
||||||
pagination: $pagination
|
|
||||||
) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
transfer {
|
|
||||||
amount
|
|
||||||
id
|
|
||||||
from
|
|
||||||
fromAccountType
|
|
||||||
to
|
|
||||||
toAccountType
|
|
||||||
asset {
|
|
||||||
id
|
|
||||||
symbol
|
|
||||||
decimals
|
|
||||||
name
|
|
||||||
quantum
|
|
||||||
status
|
|
||||||
}
|
|
||||||
reference
|
|
||||||
status
|
|
||||||
timestamp
|
|
||||||
kind {
|
|
||||||
... on RecurringTransfer {
|
|
||||||
startEpoch
|
|
||||||
endEpoch
|
|
||||||
dispatchStrategy {
|
|
||||||
dispatchMetric
|
|
||||||
dispatchMetricAssetId
|
|
||||||
marketIdsInScope
|
|
||||||
entityScope
|
|
||||||
individualScope
|
|
||||||
teamScope
|
|
||||||
nTopPerformers
|
|
||||||
stakingRequirement
|
|
||||||
notionalTimeWeightedAveragePositionRequirement
|
|
||||||
windowLength
|
|
||||||
lockPeriod
|
|
||||||
distributionStrategy
|
|
||||||
rankTable {
|
|
||||||
startRank
|
|
||||||
shareRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reason
|
|
||||||
}
|
|
||||||
fees {
|
|
||||||
transferId
|
|
||||||
amount
|
|
||||||
epoch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useActiveRewardsQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useActiveRewardsQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useActiveRewardsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useActiveRewardsQuery({
|
|
||||||
* variables: {
|
|
||||||
* isReward: // value for 'isReward'
|
|
||||||
* partyId: // value for 'partyId'
|
|
||||||
* direction: // value for 'direction'
|
|
||||||
* pagination: // value for 'pagination'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useActiveRewardsQuery(baseOptions?: Apollo.QueryHookOptions<ActiveRewardsQuery, ActiveRewardsQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<ActiveRewardsQuery, ActiveRewardsQueryVariables>(ActiveRewardsDocument, options);
|
|
||||||
}
|
|
||||||
export function useActiveRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ActiveRewardsQuery, ActiveRewardsQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<ActiveRewardsQuery, ActiveRewardsQueryVariables>(ActiveRewardsDocument, options);
|
|
||||||
}
|
|
||||||
export type ActiveRewardsQueryHookResult = ReturnType<typeof useActiveRewardsQuery>;
|
|
||||||
export type ActiveRewardsLazyQueryHookResult = ReturnType<typeof useActiveRewardsLazyQuery>;
|
|
||||||
export type ActiveRewardsQueryResult = Apollo.QueryResult<ActiveRewardsQuery, ActiveRewardsQueryVariables>;
|
|
||||||
export const RewardsHistoryDocument = gql`
|
export const RewardsHistoryDocument = gql`
|
||||||
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
|
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
|
||||||
epochRewardSummaries(
|
epochRewardSummaries(
|
||||||
@@ -322,48 +202,4 @@ export function useRewardsEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
|
|||||||
}
|
}
|
||||||
export type RewardsEpochQueryHookResult = ReturnType<typeof useRewardsEpochQuery>;
|
export type RewardsEpochQueryHookResult = ReturnType<typeof useRewardsEpochQuery>;
|
||||||
export type RewardsEpochLazyQueryHookResult = ReturnType<typeof useRewardsEpochLazyQuery>;
|
export type RewardsEpochLazyQueryHookResult = ReturnType<typeof useRewardsEpochLazyQuery>;
|
||||||
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
|
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
|
||||||
export const MarketForRewardsDocument = gql`
|
|
||||||
query MarketForRewards($marketId: ID!) {
|
|
||||||
market(id: $marketId) {
|
|
||||||
tradableInstrument {
|
|
||||||
instrument {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
code
|
|
||||||
metadata {
|
|
||||||
tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useMarketForRewardsQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useMarketForRewardsQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useMarketForRewardsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useMarketForRewardsQuery({
|
|
||||||
* variables: {
|
|
||||||
* marketId: // value for 'marketId'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useMarketForRewardsQuery(baseOptions: Apollo.QueryHookOptions<MarketForRewardsQuery, MarketForRewardsQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<MarketForRewardsQuery, MarketForRewardsQueryVariables>(MarketForRewardsDocument, options);
|
|
||||||
}
|
|
||||||
export function useMarketForRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarketForRewardsQuery, MarketForRewardsQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<MarketForRewardsQuery, MarketForRewardsQueryVariables>(MarketForRewardsDocument, options);
|
|
||||||
}
|
|
||||||
export type MarketForRewardsQueryHookResult = ReturnType<typeof useMarketForRewardsQuery>;
|
|
||||||
export type MarketForRewardsLazyQueryHookResult = ReturnType<typeof useMarketForRewardsLazyQuery>;
|
|
||||||
export type MarketForRewardsQueryResult = Apollo.QueryResult<MarketForRewardsQuery, MarketForRewardsQueryVariables>;
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import {
|
|
||||||
ActiveRewardCard,
|
|
||||||
applyFilter,
|
|
||||||
isActiveReward,
|
|
||||||
} from './active-rewards';
|
|
||||||
import {
|
|
||||||
AccountType,
|
|
||||||
AssetStatus,
|
|
||||||
DispatchMetric,
|
|
||||||
DistributionStrategy,
|
|
||||||
EntityScope,
|
|
||||||
IndividualScope,
|
|
||||||
type RecurringTransfer,
|
|
||||||
type TransferNode,
|
|
||||||
TransferStatus,
|
|
||||||
type Transfer,
|
|
||||||
} from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
jest.mock('./__generated__/Rewards', () => ({
|
|
||||||
useMarketForRewardsQuery: () => ({
|
|
||||||
data: undefined,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/assets', () => ({
|
|
||||||
useAssetDataProvider: () => {
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
assetId: 'asset-1',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ActiveRewards', () => {
|
|
||||||
const mockRecurringTransfer: RecurringTransfer = {
|
|
||||||
__typename: 'RecurringTransfer',
|
|
||||||
startEpoch: 115332,
|
|
||||||
endEpoch: 115432,
|
|
||||||
factor: '1',
|
|
||||||
dispatchStrategy: {
|
|
||||||
__typename: 'DispatchStrategy',
|
|
||||||
dispatchMetric: DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED,
|
|
||||||
dispatchMetricAssetId:
|
|
||||||
'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
|
|
||||||
marketIdsInScope: null,
|
|
||||||
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
|
|
||||||
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
|
|
||||||
teamScope: null,
|
|
||||||
nTopPerformers: '',
|
|
||||||
stakingRequirement: '',
|
|
||||||
notionalTimeWeightedAveragePositionRequirement: '',
|
|
||||||
windowLength: 1,
|
|
||||||
lockPeriod: 0,
|
|
||||||
distributionStrategy: DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
|
|
||||||
rankTable: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockTransferNode: TransferNode = {
|
|
||||||
__typename: 'TransferNode',
|
|
||||||
transfer: {
|
|
||||||
__typename: 'Transfer',
|
|
||||||
amount: '1613000000',
|
|
||||||
id: 'c4e59bd389c8098e6c7669f2d3e1613b9f42d30b1c4c3793ac44380f4c522835',
|
|
||||||
from: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
|
||||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
|
||||||
to: 'network',
|
|
||||||
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
|
||||||
asset: {
|
|
||||||
__typename: 'Asset',
|
|
||||||
id: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
|
|
||||||
symbol: 'tUSDC',
|
|
||||||
decimals: 5,
|
|
||||||
name: 'tUSDC TEST',
|
|
||||||
quantum: '1',
|
|
||||||
status: AssetStatus.STATUS_ENABLED,
|
|
||||||
source: {
|
|
||||||
__typename: 'ERC20' as const,
|
|
||||||
contractAddress: '0x123',
|
|
||||||
lifetimeLimit: '100',
|
|
||||||
withdrawThreshold: '100',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reference: 'reward',
|
|
||||||
status: TransferStatus.STATUS_PENDING,
|
|
||||||
timestamp: '2023-12-18T13:05:35.948706Z',
|
|
||||||
kind: mockRecurringTransfer,
|
|
||||||
reason: null,
|
|
||||||
},
|
|
||||||
fees: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
it('renders with valid props', () => {
|
|
||||||
render(
|
|
||||||
<ActiveRewardCard
|
|
||||||
transferNode={mockTransferNode}
|
|
||||||
currentEpoch={1}
|
|
||||||
kind={mockRecurringTransfer}
|
|
||||||
allMarkets={{}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(/Liquidity provision fees received/i)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Entity scope')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Average position')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Ends in')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Assessed over')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('1 epoch')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isActiveReward', () => {
|
|
||||||
it('returns true for valid active reward', () => {
|
|
||||||
const node = {
|
|
||||||
transfer: {
|
|
||||||
kind: {
|
|
||||||
__typename: 'RecurringTransfer',
|
|
||||||
dispatchStrategy: {},
|
|
||||||
endEpoch: 10,
|
|
||||||
},
|
|
||||||
status: TransferStatus.STATUS_PENDING,
|
|
||||||
},
|
|
||||||
} as TransferNode;
|
|
||||||
expect(isActiveReward(node, 5)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false for invalid active reward', () => {
|
|
||||||
const node = {
|
|
||||||
transfer: {
|
|
||||||
kind: {
|
|
||||||
__typename: 'RecurringTransfer',
|
|
||||||
dispatchStrategy: {},
|
|
||||||
endEpoch: 10,
|
|
||||||
},
|
|
||||||
status: TransferStatus.STATUS_PENDING,
|
|
||||||
},
|
|
||||||
} as TransferNode;
|
|
||||||
expect(isActiveReward(node, 15)).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('applyFilter', () => {
|
|
||||||
it('returns true when filter matches dispatch metric label', () => {
|
|
||||||
const transfer = {
|
|
||||||
kind: {
|
|
||||||
__typename: 'RecurringTransfer',
|
|
||||||
dispatchStrategy: {
|
|
||||||
dispatchMetric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
asset: { symbol: 'XYZ' },
|
|
||||||
} as Transfer;
|
|
||||||
const filter = { searchTerm: 'average position' };
|
|
||||||
expect(applyFilter({ transfer }, filter)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true when filter matches asset symbol', () => {
|
|
||||||
const transfer = {
|
|
||||||
kind: {
|
|
||||||
__typename: 'RecurringTransfer',
|
|
||||||
dispatchStrategy: {
|
|
||||||
dispatchMetric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
asset: { symbol: 'XYZ' },
|
|
||||||
} as Transfer;
|
|
||||||
const filter = { searchTerm: 'average position' };
|
|
||||||
expect(applyFilter({ transfer }, filter)).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,642 +0,0 @@
|
|||||||
import { useActiveRewardsQuery } from './__generated__/Rewards';
|
|
||||||
import { useT } from '../../lib/use-t';
|
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
|
||||||
type IconName,
|
|
||||||
type VegaIconSize,
|
|
||||||
Icon,
|
|
||||||
Intent,
|
|
||||||
Tooltip,
|
|
||||||
VegaIcon,
|
|
||||||
VegaIconNames,
|
|
||||||
TradingInput,
|
|
||||||
TinyScroll,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
|
||||||
import {
|
|
||||||
type Maybe,
|
|
||||||
type Transfer,
|
|
||||||
type TransferNode,
|
|
||||||
type RecurringTransfer,
|
|
||||||
DistributionStrategyDescriptionMapping,
|
|
||||||
DistributionStrategyMapping,
|
|
||||||
EntityScope,
|
|
||||||
EntityScopeMapping,
|
|
||||||
TransferStatus,
|
|
||||||
TransferStatusMapping,
|
|
||||||
DispatchMetric,
|
|
||||||
DispatchMetricDescription,
|
|
||||||
DispatchMetricLabels,
|
|
||||||
EntityScopeLabelMapping,
|
|
||||||
MarketState,
|
|
||||||
} from '@vegaprotocol/types';
|
|
||||||
import { Card } from '../card/card';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import {
|
|
||||||
type AssetFieldsFragment,
|
|
||||||
useAssetsMapProvider,
|
|
||||||
} from '@vegaprotocol/assets';
|
|
||||||
import {
|
|
||||||
type MarketFieldsFragment,
|
|
||||||
useMarketsMapProvider,
|
|
||||||
getAsset,
|
|
||||||
} from '@vegaprotocol/markets';
|
|
||||||
|
|
||||||
export type Filter = {
|
|
||||||
searchTerm: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isActiveReward = (node: TransferNode, currentEpoch: number) => {
|
|
||||||
const { transfer } = node;
|
|
||||||
if (transfer.kind.__typename !== 'RecurringTransfer') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const { dispatchStrategy } = transfer.kind;
|
|
||||||
|
|
||||||
if (!dispatchStrategy) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transfer.kind.endEpoch && transfer.kind.endEpoch < currentEpoch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transfer.status !== TransferStatus.STATUS_PENDING) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const applyFilter = (
|
|
||||||
node: TransferNode & {
|
|
||||||
asset?: AssetFieldsFragment | null;
|
|
||||||
markets?: (MarketFieldsFragment | null)[];
|
|
||||||
},
|
|
||||||
filter: Filter
|
|
||||||
) => {
|
|
||||||
const { transfer } = node;
|
|
||||||
if (
|
|
||||||
transfer.kind.__typename !== 'RecurringTransfer' ||
|
|
||||||
!transfer.kind.dispatchStrategy?.dispatchMetric
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
DispatchMetricLabels[transfer.kind.dispatchStrategy.dispatchMetric]
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(filter.searchTerm.toLowerCase()) ||
|
|
||||||
transfer.asset?.symbol
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(filter.searchTerm.toLowerCase()) ||
|
|
||||||
EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope]
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(filter.searchTerm.toLowerCase()) ||
|
|
||||||
node.asset?.name
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes(filter.searchTerm.toLowerCase()) ||
|
|
||||||
node.markets?.some((m) =>
|
|
||||||
m?.tradableInstrument?.instrument?.name
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes(filter.searchTerm.toLowerCase())
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
|
||||||
const t = useT();
|
|
||||||
const { data: activeRewardsData } = useActiveRewardsQuery({
|
|
||||||
variables: {
|
|
||||||
isReward: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [filter, setFilter] = useState<Filter>({
|
|
||||||
searchTerm: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: assets } = useAssetsMapProvider();
|
|
||||||
const { data: markets } = useMarketsMapProvider();
|
|
||||||
|
|
||||||
const enrichedTransfers = activeRewardsData?.transfersConnection?.edges
|
|
||||||
?.map((e) => e?.node as TransferNode)
|
|
||||||
.filter((node) => isActiveReward(node, currentEpoch))
|
|
||||||
.map((node) => {
|
|
||||||
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset =
|
|
||||||
assets &&
|
|
||||||
assets[
|
|
||||||
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
|
|
||||||
];
|
|
||||||
|
|
||||||
const marketsInScope =
|
|
||||||
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
|
|
||||||
(id) => markets && markets[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ...node, asset, markets: marketsInScope };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title={t('Active rewards')} className="lg:col-span-full">
|
|
||||||
{enrichedTransfers.length > 1 && (
|
|
||||||
<TradingInput
|
|
||||||
onChange={(e) =>
|
|
||||||
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
|
|
||||||
}
|
|
||||||
value={filter.searchTerm}
|
|
||||||
type="text"
|
|
||||||
placeholder={t(
|
|
||||||
'Search by reward dispatch metric, entity scope or asset name'
|
|
||||||
)}
|
|
||||||
data-testid="search-term"
|
|
||||||
className="mb-4 w-20 mr-2"
|
|
||||||
prependElement={<VegaIcon name={VegaIconNames.SEARCH} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TinyScroll className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] max-h-[40rem] overflow-auto pr-2">
|
|
||||||
{enrichedTransfers
|
|
||||||
.filter((n) => applyFilter(n, filter))
|
|
||||||
.map((node, i) => {
|
|
||||||
const { transfer } = node;
|
|
||||||
if (
|
|
||||||
transfer.kind.__typename !== 'RecurringTransfer' ||
|
|
||||||
!transfer.kind.dispatchStrategy?.dispatchMetric
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
node && (
|
|
||||||
<ActiveRewardCard
|
|
||||||
key={i}
|
|
||||||
transferNode={node}
|
|
||||||
kind={transfer.kind}
|
|
||||||
currentEpoch={currentEpoch}
|
|
||||||
allMarkets={markets || {}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TinyScroll>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// This was built to be a status indicator for the rewards based on the transfer status
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const StatusIndicator = ({
|
|
||||||
status,
|
|
||||||
reason,
|
|
||||||
}: {
|
|
||||||
status: TransferStatus;
|
|
||||||
reason?: Maybe<string> | undefined;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const getIconIntent = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case TransferStatus.STATUS_DONE:
|
|
||||||
return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
|
|
||||||
case TransferStatus.STATUS_REJECTED:
|
|
||||||
return { icon: IconNames.ERROR, intent: Intent.Danger };
|
|
||||||
default:
|
|
||||||
return { icon: IconNames.HELP, intent: Intent.Primary };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const { icon, intent } = getIconIntent(status);
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>
|
|
||||||
{t('Transfer status: {{status}} {{reason}}', {
|
|
||||||
status: TransferStatusMapping[status],
|
|
||||||
reason: reason ? `(${reason})` : '',
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
{
|
|
||||||
'text-gray-700 dark:text-gray-300': intent === Intent.None,
|
|
||||||
'text-vega-blue': intent === Intent.Primary,
|
|
||||||
'text-vega-green dark:text-vega-green': intent === Intent.Success,
|
|
||||||
'dark:text-yellow text-yellow-600': intent === Intent.Warning,
|
|
||||||
'text-vega-red': intent === Intent.Danger,
|
|
||||||
},
|
|
||||||
'flex items-start p-1 align-text-bottom'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon size={3} name={icon as IconName} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActiveRewardCard = ({
|
|
||||||
transferNode,
|
|
||||||
currentEpoch,
|
|
||||||
kind,
|
|
||||||
allMarkets,
|
|
||||||
}: {
|
|
||||||
transferNode: TransferNode & {
|
|
||||||
asset?: AssetFieldsFragment | null;
|
|
||||||
markets?: (MarketFieldsFragment | null)[];
|
|
||||||
};
|
|
||||||
currentEpoch: number;
|
|
||||||
kind: RecurringTransfer;
|
|
||||||
allMarkets?: Record<string, MarketFieldsFragment | null>;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const { transfer } = transferNode;
|
|
||||||
const { dispatchStrategy } = kind;
|
|
||||||
|
|
||||||
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
|
|
||||||
const firstMarketData = transferNode.markets?.[0];
|
|
||||||
|
|
||||||
const specificMarkets = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!firstMarketData ||
|
|
||||||
!marketIdsInScope ||
|
|
||||||
marketIdsInScope.length === 0
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (marketIdsInScope.length > 1) {
|
|
||||||
const marketNames =
|
|
||||||
allMarkets &&
|
|
||||||
marketIdsInScope
|
|
||||||
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip description={marketNames}>
|
|
||||||
<span>Specific markets</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span>{firstMarketData?.tradableInstrument?.instrument?.name || ''}</span>
|
|
||||||
);
|
|
||||||
}, [firstMarketData, marketIdsInScope, allMarkets]);
|
|
||||||
|
|
||||||
const dispatchAsset = transferNode.asset;
|
|
||||||
|
|
||||||
if (!dispatchStrategy) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gray out/hide the cards that are related to not trading markets
|
|
||||||
const marketSettled = transferNode.markets?.some(
|
|
||||||
(m) =>
|
|
||||||
m?.state &&
|
|
||||||
[
|
|
||||||
MarketState.STATE_TRADING_TERMINATED,
|
|
||||||
MarketState.STATE_SETTLED,
|
|
||||||
MarketState.STATE_CANCELLED,
|
|
||||||
MarketState.STATE_CLOSED,
|
|
||||||
].includes(m.state)
|
|
||||||
);
|
|
||||||
|
|
||||||
const assetInSettledMarket =
|
|
||||||
allMarkets &&
|
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
|
||||||
return (
|
|
||||||
m?.state &&
|
|
||||||
[
|
|
||||||
MarketState.STATE_TRADING_TERMINATED,
|
|
||||||
MarketState.STATE_SETTLED,
|
|
||||||
MarketState.STATE_CANCELLED,
|
|
||||||
MarketState.STATE_CLOSED,
|
|
||||||
].includes(m.state)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (marketSettled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
|
||||||
const suspended = transferNode.markets?.some(
|
|
||||||
(m) =>
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
|
||||||
);
|
|
||||||
|
|
||||||
const assetInSuspendedMarket =
|
|
||||||
allMarkets &&
|
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
|
||||||
return (
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
|
||||||
const { gradientClassName, mainClassName } =
|
|
||||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
|
||||||
? {
|
|
||||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
|
||||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
|
||||||
}
|
|
||||||
: getGradientClasses(dispatchStrategy.dispatchMetric);
|
|
||||||
|
|
||||||
const entityScope = dispatchStrategy.entityScope;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
|
|
||||||
'rounded-lg',
|
|
||||||
gradientClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
mainClassName,
|
|
||||||
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4 flex flex-col gap-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between gap-4">
|
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
|
||||||
<EntityIcon transfer={transfer} />
|
|
||||||
{entityScope && (
|
|
||||||
<span className="text-muted text-xs">
|
|
||||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
|
||||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
|
||||||
<span className="font-glitch">
|
|
||||||
{addDecimalsFormatNumber(
|
|
||||||
transferNode.transfer.amount,
|
|
||||||
transferNode.transfer.asset?.decimals || 0,
|
|
||||||
6
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="font-alpha">
|
|
||||||
{transferNode.transfer.asset?.symbol}
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
{
|
|
||||||
<Tooltip
|
|
||||||
description={t(
|
|
||||||
DistributionStrategyDescriptionMapping[
|
|
||||||
dispatchStrategy.distributionStrategy
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
underline={true}
|
|
||||||
>
|
|
||||||
<span className="text-xs">
|
|
||||||
{
|
|
||||||
DistributionStrategyMapping[
|
|
||||||
dispatchStrategy.distributionStrategy
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
|
||||||
<CardIcon
|
|
||||||
iconName={VegaIconNames.LOCK}
|
|
||||||
tooltip={t(
|
|
||||||
'Number of epochs after distribution to delay vesting of rewards by'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="text-muted text-xs whitespace-nowrap">
|
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
|
||||||
count: kind.dispatchStrategy?.lockPeriod,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
|
||||||
<span>
|
|
||||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
|
||||||
<Tooltip
|
|
||||||
underline={suspended}
|
|
||||||
description={
|
|
||||||
(suspended || assetInSuspendedMarket) &&
|
|
||||||
(specificMarkets
|
|
||||||
? t('Eligible market(s) currently suspended')
|
|
||||||
: assetInSuspendedMarket
|
|
||||||
? t('Currently no markets eligible for reward')
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>{specificMarkets || dispatchAsset?.name}</span>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-8 flex-wrap">
|
|
||||||
{kind.endEpoch && (
|
|
||||||
<span className="flex flex-col">
|
|
||||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
|
||||||
<span>
|
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
|
||||||
count: kind.endEpoch - currentEpoch,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{
|
|
||||||
<span className="flex flex-col">
|
|
||||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
|
||||||
<span>
|
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
|
||||||
count: dispatchStrategy.windowLength,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{dispatchStrategy?.dispatchMetric && (
|
|
||||||
<span className="text-muted text-sm h-[2rem]">
|
|
||||||
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
|
||||||
|
|
||||||
<div className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
|
||||||
<span className="flex flex-col gap-1">
|
|
||||||
<span className="flex items-center gap-1 text-muted">
|
|
||||||
{t('Entity scope')}{' '}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{kind.dispatchStrategy?.teamScope && (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>{kind.dispatchStrategy?.teamScope}</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
|
||||||
{<VegaIcon name={VegaIconNames.TEAM} size={16} />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{kind.dispatchStrategy?.individualScope && (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>{kind.dispatchStrategy?.individualScope}</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
|
||||||
{<VegaIcon name={VegaIconNames.MAN} size={16} />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{/* Shows transfer status */}
|
|
||||||
{/* <StatusIndicator
|
|
||||||
status={transfer.status}
|
|
||||||
reason={transfer.reason}
|
|
||||||
/> */}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col gap-1">
|
|
||||||
<span className="flex items-center gap-1 text-muted">
|
|
||||||
{t('Staked VEGA')}{' '}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{addDecimalsFormatNumber(
|
|
||||||
kind.dispatchStrategy?.stakingRequirement || 0,
|
|
||||||
transfer.asset?.decimals || 0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col gap-1">
|
|
||||||
<span className="flex items-center gap-1 text-muted">
|
|
||||||
{t('Average position')}{' '}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{addDecimalsFormatNumber(
|
|
||||||
kind.dispatchStrategy
|
|
||||||
?.notionalTimeWeightedAveragePositionRequirement || 0,
|
|
||||||
transfer.asset?.decimals || 0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGradientClasses = (d: DispatchMetric | undefined) => {
|
|
||||||
switch (d) {
|
|
||||||
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
|
|
||||||
return {
|
|
||||||
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
|
|
||||||
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
|
|
||||||
};
|
|
||||||
case DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED:
|
|
||||||
return {
|
|
||||||
gradientClassName: 'from-vega-green-500 to-vega-yellow-500',
|
|
||||||
mainClassName: 'from-vega-green-400 dark:from-vega-green-600 to-20%',
|
|
||||||
};
|
|
||||||
case DispatchMetric.DISPATCH_METRIC_MAKER_FEES_PAID:
|
|
||||||
return {
|
|
||||||
gradientClassName: 'from-vega-orange-500 to-vega-pink-400',
|
|
||||||
mainClassName: 'from-vega-orange-400 dark:from-vega-orange-600 to-20%',
|
|
||||||
};
|
|
||||||
case DispatchMetric.DISPATCH_METRIC_MARKET_VALUE:
|
|
||||||
case DispatchMetric.DISPATCH_METRIC_RELATIVE_RETURN:
|
|
||||||
return {
|
|
||||||
gradientClassName: 'from-vega-purple-500 to-vega-blue-400',
|
|
||||||
mainClassName: 'from-vega-purple-400 dark:from-vega-purple-600 to-20%',
|
|
||||||
};
|
|
||||||
case DispatchMetric.DISPATCH_METRIC_RETURN_VOLATILITY:
|
|
||||||
return {
|
|
||||||
gradientClassName: 'from-vega-blue-500 to-vega-green-400',
|
|
||||||
mainClassName: 'from-vega-blue-400 dark:from-vega-blue-600 to-20%',
|
|
||||||
};
|
|
||||||
case DispatchMetric.DISPATCH_METRIC_VALIDATOR_RANKING:
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
|
|
||||||
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const CardIcon = ({
|
|
||||||
size = 18,
|
|
||||||
iconName,
|
|
||||||
tooltip,
|
|
||||||
}: {
|
|
||||||
size?: VegaIconSize;
|
|
||||||
iconName: VegaIconNames;
|
|
||||||
tooltip: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Tooltip description={<span>{tooltip}</span>}>
|
|
||||||
<span className="flex items-center p-2 rounded-full border border-gray-600">
|
|
||||||
<VegaIcon name={iconName} size={size} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityIcon = ({
|
|
||||||
transfer,
|
|
||||||
size = 18,
|
|
||||||
}: {
|
|
||||||
transfer: Transfer;
|
|
||||||
size?: VegaIconSize;
|
|
||||||
}) => {
|
|
||||||
if (transfer.kind.__typename !== 'RecurringTransfer') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const entityScope = transfer.kind.dispatchStrategy?.entityScope;
|
|
||||||
const getIconName = () => {
|
|
||||||
switch (entityScope) {
|
|
||||||
case EntityScope.ENTITY_SCOPE_TEAMS:
|
|
||||||
return VegaIconNames.TEAM;
|
|
||||||
case EntityScope.ENTITY_SCOPE_INDIVIDUALS:
|
|
||||||
return VegaIconNames.MAN;
|
|
||||||
default:
|
|
||||||
return VegaIconNames.QUESTION_MARK;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const iconName = getIconName();
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>{entityScope ? EntityScopeMapping[entityScope] : ''}</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex items-center p-2 rounded-full border border-gray-600">
|
|
||||||
{iconName && <VegaIcon name={iconName} size={size} />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -33,10 +33,6 @@ import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
|||||||
import { RewardsHistoryContainer } from './rewards-history';
|
import { RewardsHistoryContainer } from './rewards-history';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||||
import { ActiveRewards } from './active-rewards';
|
|
||||||
import { ActivityStreak } from './streaks/activity-streaks';
|
|
||||||
import { RewardHoarderBonus } from './streaks/reward-hoarder-bonus';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
|
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
|
||||||
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
|
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
|
||||||
@@ -49,7 +45,6 @@ export const RewardsContainer = () => {
|
|||||||
const { params, loading: paramsLoading } = useNetworkParams([
|
const { params, loading: paramsLoading } = useNetworkParams([
|
||||||
NetworkParams.reward_asset,
|
NetworkParams.reward_asset,
|
||||||
NetworkParams.rewards_activityStreak_benefitTiers,
|
NetworkParams.rewards_activityStreak_benefitTiers,
|
||||||
NetworkParams.rewards_vesting_benefitTiers,
|
|
||||||
NetworkParams.rewards_vesting_baseRate,
|
NetworkParams.rewards_vesting_baseRate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -59,14 +54,6 @@ export const RewardsContainer = () => {
|
|||||||
|
|
||||||
const { data: epochData } = useRewardsEpochQuery();
|
const { data: epochData } = useRewardsEpochQuery();
|
||||||
|
|
||||||
const { rewards_activityStreak_benefitTiers, rewards_vesting_benefitTiers } =
|
|
||||||
params || {};
|
|
||||||
|
|
||||||
const activityStreakBenefitTiers = JSON.parse(
|
|
||||||
rewards_activityStreak_benefitTiers
|
|
||||||
);
|
|
||||||
const vestingBenefitTiers = JSON.parse(rewards_vesting_benefitTiers);
|
|
||||||
|
|
||||||
// No need to specify the fromEpoch as it will by default give you the last
|
// No need to specify the fromEpoch as it will by default give you the last
|
||||||
// Note activityStreak in query will fail
|
// Note activityStreak in query will fail
|
||||||
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
||||||
@@ -81,9 +68,6 @@ export const RewardsContainer = () => {
|
|||||||
pollInterval: 10000,
|
pollInterval: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const partyActivityStreak = rewardsData?.party?.activityStreak;
|
|
||||||
const vestingDetails = rewardsData?.party?.vestingStats;
|
|
||||||
|
|
||||||
if (!epochData?.epoch || !assetMap) return null;
|
if (!epochData?.epoch || !assetMap) return null;
|
||||||
|
|
||||||
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
||||||
@@ -130,98 +114,74 @@ export const RewardsContainer = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full gap-3">
|
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
{/* Always show reward information for vega */}
|
||||||
{/* Always show reward information for vega */}
|
<Card
|
||||||
<Card
|
key={params.reward_asset}
|
||||||
key={params.reward_asset}
|
title={t('Vega Reward pot')}
|
||||||
title={t('Vega Reward pot')}
|
className="lg:col-span-3 xl:col-span-2"
|
||||||
className="lg:col-span-3 xl:col-span-2"
|
loading={loading}
|
||||||
loading={loading}
|
highlight={true}
|
||||||
highlight={true}
|
>
|
||||||
>
|
<RewardPot
|
||||||
<RewardPot
|
pubKey={pubKey}
|
||||||
pubKey={pubKey}
|
accounts={accounts}
|
||||||
accounts={accounts}
|
assetId={params.reward_asset}
|
||||||
assetId={params.reward_asset}
|
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
|
||||||
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
|
/>
|
||||||
/>
|
</Card>
|
||||||
</Card>
|
<Card
|
||||||
<Card
|
title={t('Vesting')}
|
||||||
title={t('Vesting')}
|
className="lg:col-span-3 xl:col-span-2"
|
||||||
className="lg:col-span-3 xl:col-span-2"
|
loading={loading}
|
||||||
loading={loading}
|
>
|
||||||
>
|
<Vesting
|
||||||
<Vesting
|
pubKey={pubKey}
|
||||||
pubKey={pubKey}
|
baseRate={params.rewards_vesting_baseRate}
|
||||||
baseRate={params.rewards_vesting_baseRate}
|
multiplier={
|
||||||
multiplier={
|
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
|
||||||
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Card>
|
||||||
</Card>
|
<Card
|
||||||
<Card
|
title={t('Rewards multipliers')}
|
||||||
title={t('Rewards multipliers')}
|
className="lg:col-span-3 xl:col-span-2"
|
||||||
className="lg:col-span-3 xl:col-span-2"
|
loading={loading}
|
||||||
loading={loading}
|
highlight={true}
|
||||||
highlight={true}
|
>
|
||||||
>
|
<Multipliers
|
||||||
<Multipliers
|
pubKey={pubKey}
|
||||||
pubKey={pubKey}
|
hoarderMultiplier={
|
||||||
hoarderMultiplier={
|
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
|
||||||
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
|
}
|
||||||
}
|
streakMultiplier={
|
||||||
streakMultiplier={
|
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
|
||||||
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Card>
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||||
{assets
|
{assets
|
||||||
.filter((assetId) => assetId !== params.reward_asset)
|
.filter((assetId) => assetId !== params.reward_asset)
|
||||||
.map((assetId) => {
|
.map((assetId) => {
|
||||||
const asset = assetMap ? assetMap[assetId] : null;
|
const asset = assetMap ? assetMap[assetId] : null;
|
||||||
|
|
||||||
if (!asset) return null;
|
if (!asset) return null;
|
||||||
|
|
||||||
// Following code is for mitigating an issue due to a core bug where locked and vesting
|
// Following code is for mitigating an issue due to a core bug where locked and vesting
|
||||||
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
|
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
|
||||||
//
|
//
|
||||||
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
|
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
|
||||||
// show the user that they have rewards available to withdraw
|
// show the user that they have rewards available to withdraw
|
||||||
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
|
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
|
||||||
const accountsForAsset = rewardAccountsAssetMap[asset.id];
|
const accountsForAsset = rewardAccountsAssetMap[asset.id];
|
||||||
const vestedAccount = accountsForAsset?.find(
|
const vestedAccount = accountsForAsset?.find(
|
||||||
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||||
);
|
);
|
||||||
|
|
||||||
// No vested rewards available to withdraw, so skip over USDT
|
// No vested rewards available to withdraw, so skip over USDT
|
||||||
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
|
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={assetId}
|
|
||||||
title={t('{{assetSymbol}} Reward pot', {
|
|
||||||
assetSymbol: asset.symbol,
|
|
||||||
})}
|
|
||||||
className="lg:col-span-3 xl:col-span-2"
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
<RewardPot
|
|
||||||
pubKey={pubKey}
|
|
||||||
accounts={accounts}
|
|
||||||
assetId={assetId}
|
|
||||||
// Ensure that these values are shown as 0
|
|
||||||
vestingBalancesSummary={{
|
|
||||||
lockedBalances: [],
|
|
||||||
vestingBalances: [],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -237,69 +197,48 @@ export const RewardsContainer = () => {
|
|||||||
pubKey={pubKey}
|
pubKey={pubKey}
|
||||||
accounts={accounts}
|
accounts={accounts}
|
||||||
assetId={assetId}
|
assetId={assetId}
|
||||||
vestingBalancesSummary={
|
// Ensure that these values are shown as 0
|
||||||
rewardsData?.party?.vestingBalancesSummary
|
vestingBalancesSummary={{
|
||||||
}
|
lockedBalances: [],
|
||||||
|
vestingBalances: [],
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
</div>
|
|
||||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
return (
|
||||||
{pubKey && activityStreakBenefitTiers.tiers?.length > 0 && (
|
<Card
|
||||||
<Card
|
key={assetId}
|
||||||
title={t('Activity Streak')}
|
title={t('{{assetSymbol}} Reward pot', {
|
||||||
className={classNames(
|
assetSymbol: asset.symbol,
|
||||||
{
|
})}
|
||||||
'lg:col-span-6 xl:col-span-3':
|
className="lg:col-span-3 xl:col-span-2"
|
||||||
activityStreakBenefitTiers.tiers.length <= 4,
|
loading={loading}
|
||||||
'xl:col-span-6': activityStreakBenefitTiers.tiers.length > 4,
|
>
|
||||||
},
|
<RewardPot
|
||||||
'hidden md:block'
|
pubKey={pubKey}
|
||||||
)}
|
accounts={accounts}
|
||||||
>
|
assetId={assetId}
|
||||||
<span className="flex flex-col mr-8 pr-4">
|
vestingBalancesSummary={
|
||||||
<ActivityStreak
|
rewardsData?.party?.vestingBalancesSummary
|
||||||
tiers={activityStreakBenefitTiers.tiers}
|
}
|
||||||
streak={partyActivityStreak}
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</Card>
|
||||||
</Card>
|
);
|
||||||
)}
|
})}
|
||||||
{pubKey && vestingBenefitTiers.tiers?.length > 0 && (
|
<Card
|
||||||
<Card
|
title={t('Rewards history')}
|
||||||
title={t('Reward Hoarder Bonus')}
|
className="lg:col-span-full"
|
||||||
className={classNames(
|
loading={rewardsLoading}
|
||||||
{
|
noBackgroundOnMobile={true}
|
||||||
'lg:col-span-6 xl:col-span-3':
|
>
|
||||||
vestingBenefitTiers.tiers.length <= 4,
|
<RewardsHistoryContainer
|
||||||
'xl:col-span-6': vestingBenefitTiers.tiers.length > 4,
|
epoch={Number(epochData?.epoch.id)}
|
||||||
},
|
pubKey={pubKey}
|
||||||
'hidden md:block'
|
assets={assetMap}
|
||||||
)}
|
/>
|
||||||
>
|
</Card>
|
||||||
<span className="flex flex-col mr-8 pr-4">
|
|
||||||
<RewardHoarderBonus
|
|
||||||
tiers={vestingBenefitTiers.tiers}
|
|
||||||
vestingDetails={vestingDetails}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
<ActiveRewards currentEpoch={Number(epochData?.epoch.id)} />
|
|
||||||
<Card
|
|
||||||
title={t('Rewards history')}
|
|
||||||
className="lg:col-span-full hidden md:block"
|
|
||||||
loading={rewardsLoading}
|
|
||||||
noBackgroundOnMobile={true}
|
|
||||||
>
|
|
||||||
<RewardsHistoryContainer
|
|
||||||
epoch={Number(epochData?.epoch.id)}
|
|
||||||
pubKey={pubKey}
|
|
||||||
assets={assetMap}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -404,7 +343,7 @@ export const RewardPot = ({
|
|||||||
})}
|
})}
|
||||||
<VegaIcon name={VegaIconNames.LOCK} size={12} />
|
<VegaIcon name={VegaIconNames.LOCK} size={12} />
|
||||||
</CardTableTH>
|
</CardTableTH>
|
||||||
<CardTableTD data-testid="locked-value">
|
<CardTableTD>
|
||||||
{addDecimalsFormatNumberQuantum(
|
{addDecimalsFormatNumberQuantum(
|
||||||
totalLocked.toString(),
|
totalLocked.toString(),
|
||||||
rewardAsset.decimals,
|
rewardAsset.decimals,
|
||||||
@@ -418,7 +357,7 @@ export const RewardPot = ({
|
|||||||
assetSymbol: rewardAsset.symbol,
|
assetSymbol: rewardAsset.symbol,
|
||||||
})}
|
})}
|
||||||
</CardTableTH>
|
</CardTableTH>
|
||||||
<CardTableTD data-testid="vesting-value">
|
<CardTableTD>
|
||||||
{addDecimalsFormatNumberQuantum(
|
{addDecimalsFormatNumberQuantum(
|
||||||
totalVesting.toString(),
|
totalVesting.toString(),
|
||||||
rewardAsset.decimals,
|
rewardAsset.decimals,
|
||||||
@@ -430,7 +369,7 @@ export const RewardPot = ({
|
|||||||
<CardTableTH>
|
<CardTableTH>
|
||||||
{t('Available to withdraw this epoch')}
|
{t('Available to withdraw this epoch')}
|
||||||
</CardTableTH>
|
</CardTableTH>
|
||||||
<CardTableTD data-testid="available-to-withdraw-value">
|
<CardTableTD>
|
||||||
{addDecimalsFormatNumberQuantum(
|
{addDecimalsFormatNumberQuantum(
|
||||||
totalVestedRewardsByRewardAsset.toString(),
|
totalVestedRewardsByRewardAsset.toString(),
|
||||||
rewardAsset.decimals,
|
rewardAsset.decimals,
|
||||||
@@ -449,7 +388,6 @@ export const RewardPot = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
size="small"
|
size="small"
|
||||||
data-testid="redeem-rewards-button"
|
|
||||||
>
|
>
|
||||||
{t('Redeem rewards')}
|
{t('Redeem rewards')}
|
||||||
</TradingButton>
|
</TradingButton>
|
||||||
@@ -484,16 +422,12 @@ export const Vesting = ({
|
|||||||
<CardTable>
|
<CardTable>
|
||||||
<tr>
|
<tr>
|
||||||
<CardTableTH>{t('Base rate')}</CardTableTH>
|
<CardTableTH>{t('Base rate')}</CardTableTH>
|
||||||
<CardTableTD data-testid="base-rate-value">
|
<CardTableTD>{baseRateFormatted}%</CardTableTD>
|
||||||
{baseRateFormatted}%
|
|
||||||
</CardTableTD>
|
|
||||||
</tr>
|
</tr>
|
||||||
{pubKey && (
|
{pubKey && (
|
||||||
<tr>
|
<tr>
|
||||||
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
||||||
<CardTableTD data-testid="vesting multiplier-value">
|
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
|
||||||
{multiplier ? `${multiplier}x` : '-'}
|
|
||||||
</CardTableTD>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</CardTable>
|
</CardTable>
|
||||||
@@ -533,13 +467,13 @@ export const Multipliers = ({
|
|||||||
<CardTable>
|
<CardTable>
|
||||||
<tr>
|
<tr>
|
||||||
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
||||||
<CardTableTD data-testid="streak-reward-multiplier-value">
|
<CardTableTD>
|
||||||
{streakMultiplier ? `${streakMultiplier}x` : '-'}
|
{streakMultiplier ? `${streakMultiplier}x` : '-'}
|
||||||
</CardTableTD>
|
</CardTableTD>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
||||||
<CardTableTD data-testid="hoarder-reward-multiplier-value">
|
<CardTableTD>
|
||||||
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
|
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
|
||||||
</CardTableTD>
|
</CardTableTD>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -319,7 +319,6 @@ export const RewardHistoryTable = ({
|
|||||||
onClick={() => setIsParty(false)}
|
onClick={() => setIsParty(false)}
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
minimal={isParty}
|
minimal={isParty}
|
||||||
data-testid="total-distributed-button"
|
|
||||||
>
|
>
|
||||||
{t('Total distributed')}
|
{t('Total distributed')}
|
||||||
</TradingButton>
|
</TradingButton>
|
||||||
@@ -328,7 +327,6 @@ export const RewardHistoryTable = ({
|
|||||||
size="extra-small"
|
size="extra-small"
|
||||||
disabled={!pubKey}
|
disabled={!pubKey}
|
||||||
minimal={!isParty}
|
minimal={!isParty}
|
||||||
data-testid="earned-by-me-button"
|
|
||||||
>
|
>
|
||||||
{t('Earned by me')}
|
{t('Earned by me')}
|
||||||
</TradingButton>
|
</TradingButton>
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { ActivityStreak } from './activity-streaks';
|
|
||||||
|
|
||||||
describe('ActivityStreak', () => {
|
|
||||||
it('renders null when streak is not active', () => {
|
|
||||||
const tiers: {
|
|
||||||
minimum_activity_streak: number;
|
|
||||||
reward_multiplier: string;
|
|
||||||
vesting_multiplier: string;
|
|
||||||
}[] = [];
|
|
||||||
const streak = null;
|
|
||||||
render(<ActivityStreak tiers={tiers} streak={streak} />);
|
|
||||||
const component = screen.queryByText(/epochs streak/i);
|
|
||||||
expect(component).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders null when tiers are empty', () => {
|
|
||||||
const tiers: {
|
|
||||||
minimum_activity_streak: number;
|
|
||||||
reward_multiplier: string;
|
|
||||||
vesting_multiplier: string;
|
|
||||||
}[] = [];
|
|
||||||
const streak = {
|
|
||||||
activeFor: 10,
|
|
||||||
isActive: true,
|
|
||||||
inactiveFor: 10,
|
|
||||||
rewardDistributionMultiplier: '45678',
|
|
||||||
rewardVestingMultiplier: '45678',
|
|
||||||
epoch: 10,
|
|
||||||
tradedVolume: '45678',
|
|
||||||
openVolume: '45678',
|
|
||||||
};
|
|
||||||
render(<ActivityStreak tiers={tiers} streak={streak} />);
|
|
||||||
const component = screen.queryByText(/epochs streak/i);
|
|
||||||
expect(component).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders the component with tiers and active streak', () => {
|
|
||||||
const tiers = [
|
|
||||||
{
|
|
||||||
minimum_activity_streak: 5,
|
|
||||||
reward_multiplier: '1.5x',
|
|
||||||
vesting_multiplier: '2x',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
minimum_activity_streak: 10,
|
|
||||||
reward_multiplier: '2x',
|
|
||||||
vesting_multiplier: '3x',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const streak = {
|
|
||||||
activeFor: 7,
|
|
||||||
isActive: true,
|
|
||||||
inactiveFor: 10,
|
|
||||||
rewardDistributionMultiplier: '45678',
|
|
||||||
rewardVestingMultiplier: '45678',
|
|
||||||
epoch: 10,
|
|
||||||
tradedVolume: '45678',
|
|
||||||
openVolume: '45678',
|
|
||||||
};
|
|
||||||
render(<ActivityStreak tiers={tiers} streak={streak} />);
|
|
||||||
|
|
||||||
const tierLabels = screen.getAllByText(/Tier/i);
|
|
||||||
expect(tierLabels.length).toBe(3); // 2 tiers + 1 label
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useT } from '../../../lib/use-t';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import type { PartyActivityStreak } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
export const safeProgress = (
|
|
||||||
i: number,
|
|
||||||
userTierIndex: number,
|
|
||||||
total: number | string,
|
|
||||||
progress?: number | string
|
|
||||||
) => {
|
|
||||||
if (i < userTierIndex) return 100;
|
|
||||||
if (i > userTierIndex) return 0;
|
|
||||||
|
|
||||||
if (!progress || !total) return 0;
|
|
||||||
if (new BigNumber(progress).isGreaterThan(total)) return 100;
|
|
||||||
return new BigNumber(progress)
|
|
||||||
.multipliedBy(100)
|
|
||||||
.dividedBy(total || 1)
|
|
||||||
.toNumber();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetUserTier = (
|
|
||||||
tiers: {
|
|
||||||
minimum_activity_streak: number;
|
|
||||||
reward_multiplier: string;
|
|
||||||
vesting_multiplier: string;
|
|
||||||
}[],
|
|
||||||
progress?: number
|
|
||||||
) => {
|
|
||||||
if (!progress) return 0;
|
|
||||||
if (!tiers || tiers.length === 0) return 0;
|
|
||||||
|
|
||||||
let userTier = 0;
|
|
||||||
let i = 0;
|
|
||||||
while (
|
|
||||||
i < tiers.length &&
|
|
||||||
tiers[userTier].minimum_activity_streak < progress
|
|
||||||
) {
|
|
||||||
userTier = i;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
i === tiers.length &&
|
|
||||||
tiers[userTier].minimum_activity_streak <= progress
|
|
||||||
) {
|
|
||||||
userTier = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userTier > tiers.length) {
|
|
||||||
userTier--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userTier;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActivityStreak = ({
|
|
||||||
tiers,
|
|
||||||
streak,
|
|
||||||
}: {
|
|
||||||
tiers: {
|
|
||||||
minimum_activity_streak: number;
|
|
||||||
reward_multiplier: string;
|
|
||||||
vesting_multiplier: string;
|
|
||||||
}[];
|
|
||||||
streak?: PartyActivityStreak | null;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const userTierIndex = useGetUserTier(tiers, streak?.activeFor);
|
|
||||||
|
|
||||||
if (!tiers || tiers.length === 0) return null;
|
|
||||||
|
|
||||||
const progressBarHeight = 'h-10';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-1 w-full">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div
|
|
||||||
className="grid"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns:
|
|
||||||
'repeat(' + tiers.length + ', minmax(0, 1fr))',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tiers.map((tier, index) => {
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex justify-end -mr-[2.85rem]">
|
|
||||||
<span className="flex flex-col items-center gap-4 justify-between">
|
|
||||||
<span className="flex flex-col items-center gap-1">
|
|
||||||
<span className="flex flex-col items-center font-medium">
|
|
||||||
<span className="text-sm">
|
|
||||||
{t('Tier {{tier}}', {
|
|
||||||
tier: index + 1,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="text-muted text-xs">
|
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
|
||||||
count: tier.minimum_activity_streak,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'text-xs flex flex-col items-center justify-center px-2 py-1 rounded-lg text-white border',
|
|
||||||
{
|
|
||||||
'border-pink-600 bg-pink-900': index % 6 === 0,
|
|
||||||
'border-purple-600 bg-purple-900': index % 6 === 1,
|
|
||||||
'border-blue-600 bg-blue-900': index % 6 === 2,
|
|
||||||
'border-orange-600 bg-orange-900': index % 6 === 3,
|
|
||||||
'border-green-600 bg-green-900': index % 6 === 4,
|
|
||||||
'border-yellow-600 bg-yellow-900': index % 6 === 5,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{t('Reward {{reward}}x', {
|
|
||||||
reward: tier.reward_multiplier,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{t('Vesting {{vesting}}x', {
|
|
||||||
vesting: tier.vesting_multiplier,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
{
|
|
||||||
'text-pink-500': index % 6 === 0,
|
|
||||||
'text-purple-500': index % 6 === 1,
|
|
||||||
'text-blue-500': index % 6 === 2,
|
|
||||||
'text-orange-500': index % 6 === 3,
|
|
||||||
'text-green-500': index % 6 === 4,
|
|
||||||
'text-yellow-500': index % 6 === 5,
|
|
||||||
},
|
|
||||||
'leading-[0] font-sans text-[48px]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
•
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{tiers.map((_tier, index) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="bg-white dark:bg-gray-800 shadow-card rounded-[100px] grow"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'relative w-full rounded-[100px] bg-gray-200 dark:bg-gray-800',
|
|
||||||
progressBarHeight
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'absolute left-0 top-0 h-full rounded-[100px] bg-gradient-to-r',
|
|
||||||
{
|
|
||||||
'from-vega-dark-400 to-vega-dark-200':
|
|
||||||
userTierIndex === 0 || streak?.isActive === false,
|
|
||||||
'from-vega-pink-600 to-vega-pink-500':
|
|
||||||
userTierIndex % 6 === 1,
|
|
||||||
'from-vega-purple-600 to-vega-purple-500':
|
|
||||||
userTierIndex % 6 === 2,
|
|
||||||
'from-vega-blue-600 to-vega-blue-500':
|
|
||||||
userTierIndex % 6 === 3,
|
|
||||||
'from-vega-orange-600 to-vega-orange-500':
|
|
||||||
userTierIndex % 6 === 4,
|
|
||||||
'from-vega-green-600 to-vega-green-500':
|
|
||||||
userTierIndex % 6 === 5,
|
|
||||||
'from-vega-yellow-600 to-vega-yellow-500':
|
|
||||||
userTierIndex % 6 === 0,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
width:
|
|
||||||
safeProgress(
|
|
||||||
index,
|
|
||||||
userTierIndex,
|
|
||||||
tiers[index].minimum_activity_streak,
|
|
||||||
streak?.activeFor
|
|
||||||
) + '%',
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<VegaIcon name={VegaIconNames.STREAK} />
|
|
||||||
|
|
||||||
<span className="flex flex-col">
|
|
||||||
{streak?.isActive && (
|
|
||||||
<span data-testid="epoch-streak">
|
|
||||||
{t('userActive', '{{active}} trader: {{count}} epochs so far', {
|
|
||||||
active: streak?.isActive ? 'Active' : 'Inactive',
|
|
||||||
count: streak?.activeFor || 0,
|
|
||||||
})}{' '}
|
|
||||||
{userTierIndex > 0 &&
|
|
||||||
new BigNumber(
|
|
||||||
tiers[0].minimum_activity_streak
|
|
||||||
).isLessThanOrEqualTo(streak?.activeFor || 0) &&
|
|
||||||
t('(Tier {{tier}} as of last epoch)', {
|
|
||||||
tier: userTierIndex,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user