Compare commits

..
Author SHA1 Message Date
Madalina Raicu 18f8d5e648 fix(trading): adjust full screen for mobile dialogs 2024-01-24 17:20:40 +00:00
261f32aa5b feat(trading): margin mode selector (#5660)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-01-24 13:17:22 +00:00
Edd 053775bef6 fix(governance): handle an expected error (#5653) 2024-01-24 11:41:40 +00:00
Matthew Russellandbwallacee 0660eda334 chore(trading): store all chart state (#5627)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-01-24 10:21:33 +00:00
Matthew Russell f22a3bc2d2 Revert "feat(trading): margin mode selector (#5575)"
This reverts commit fde77ebccb.
2024-01-23 08:46:16 -08:00
Bartłomiej GłowniaandDariusz Majcherczyk fde77ebccb feat(trading): margin mode selector (#5575)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-01-23 16:19:49 +00:00
e309669736 fix(trading): typo translation full featured link (#5659)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-01-23 16:05:51 +00:00
b1621d1191 chore(trading): fix monitoring bounds (#5655) (#5657)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-01-23 15:45:05 +00:00
ArtandDexter Edwards 39907f07db feat(wallet): new browser wallet connection model (#5572)
Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
2024-01-23 15:32:23 +00:00
3dab5f3d9b chore(trading): merge main back in develop (#5654)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-01-23 14:57:20 +00:00
Edd 3cf9ae7582 fix(governance): handle null validator scores properly (#5459) 2024-01-23 14:46:19 +00:00
Ben 557894e2ef chore(trading): turn off parallel (#5650) 2024-01-23 14:43:51 +00:00
m.rayandbwallacee baf9875c69 feat(trading): filter out suspended transfers (#5640)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-01-23 09:38:09 +00:00
Art 51199b02ce feat(governance): update market proposal header with copy and open buttons (#5648) 2024-01-23 10:13:56 +01:00
m.ray bb826c88f0 fix(trading): set trading view as default (#5632) 2024-01-22 16:09:18 +00:00
Edd 0da20b750f feat(explorer): referral and team transaction support (#5623) 2024-01-22 10:53:53 +00:00
m.rayandDariusz Majcherczyk e16c447564 chore(trading): remove quadratic slippage factor (#5641)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-01-22 10:17:16 +00:00
Bartłomiej GłowniaandDariusz Majcherczyk a2170d27d6 feat(ui-toolkit): update slider design (#5645)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-01-22 08:30:57 +01:00
Ben 00d840b7b3 chore(trading): delete and move some tests to jest (#5633) 2024-01-19 14:15:53 +00:00
Ben 2a4a05630f chore(trading): fix skipped tests (#5638) 2024-01-19 08:20:52 +00:00
Ben 97f1f40f2c chore(trading): remove waits and tests (#5637) 2024-01-18 13:20:58 +00:00
Bartłomiej GłowniaandMatthew Russell a8e6963521 feat(deposits): if wrong network dont show the form and prompt switching to the correct network (#5571)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-01-18 10:41:19 +01:00
m.ray a2bffa1dfd fix(trading): alphabetically order translations in trading.json (#5635) 2024-01-17 21:28:20 +00:00
Ben 9253e8067a chore(trading): market-sim branch parralel fix (#5630) 2024-01-17 14:44:11 +00:00
Bartłomiej Głownia 58972f0a11 feat(ui-toolkit): add leverage slider component (#5594) 2024-01-17 13:46:11 +00:00
Ben a3c55fd7c3 chore(trading): update vega-market-sim to use to 0.74.0-preview.2 (#5603) 2024-01-17 12:19:52 +00:00
Matthew Russell c63cba1071 chore(trading): enable trading view on mainnet (#5628) 2024-01-16 18:16:58 +00:00
m.ray bc9d87fe30 chore(trading): clean up triggering ratio (#5604) 2024-01-16 18:10:32 +01:00
m.ray 6a21862378 fix(trading): use No trading instead of Trading terminated (#5624) 2024-01-16 18:30:42 +02:00
Ben 0fb4348e9c chore(trading): upload market-sim logs (#5622) 2024-01-15 20:56:52 +00:00
m.ray b751bcf17d feat(trading): mobile responsiveness tweaks (#5613) 2024-01-15 15:26:13 +00:00
m.ray d32f27fcb1 feat(trading): mobile responsiveness - market selector (#5582) 2024-01-11 11:23:48 +00:00
m.ray d05cd6a2ed fix(trading): tiny scroll for rewards (#5586) 2024-01-10 15:49:58 +00:00
m.ray c003e5fa30 fix(trading): liquidity table improve readability and remove grouping (#5598) 2024-01-10 15:49:35 +00:00
Ben f62d3289ab chore(trading): retry on http error (#5601) 2024-01-10 15:44:01 +00:00
Ben a3d3d18c5c chore(trading): fix price monitoring test (#5600) 2024-01-10 14:56:17 +00:00
77ca101781 chore(trading): merge main back in develop (#5597)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-01-10 10:31:15 +00:00
Ben d238c37d0c chore(trading): vesting test (#5595) 2024-01-10 10:07:59 +00:00
Edd 79feb485f6 fix(governance): improve error message for configuration error (#5589) 2024-01-09 17:21:56 +00:00
Ben 6aa5c3b6e3 chore(trading): rewards page e2e (#5578) 2024-01-09 15:54:40 +00:00
Ben 82abc13fda chore(trading): skip test due to issue (#5583) 2024-01-08 16:16:03 +00:00
Edd 933f07cddf fix(explorer): minor tidyup of deterministic order view (#5573) 2024-01-08 13:25:03 +00:00
Matthew Russell c8067669c2 fix(trading): ensure fees queries always fetch on mount and poll (#5566) 2024-01-05 12:43:30 +00:00
78f5a9c520 feat(trading): activity streaks, reward hoarder bonus and active rewards (#5491)
Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: bwallacee <ben@vega.xyz>
2024-01-05 11:16:59 +00:00
Ben 462066959d chore(trading): update vega (#5563) 2024-01-03 12:00:00 +00:00
daro-maj f12f5ab961 chore(trading): fix volume issue for market selector tests (#5564) 2024-01-03 12:04:49 +01:00
98b2460d8b fix(trading): mobile friendly referral and fees views (#5530)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-01-02 15:50:04 +00:00
Matthew Russell 67c182f3de chore(trading-view): minor tv chart tweaks (#5545) 2024-01-02 13:20:01 +00:00
68e0009ab0 feat(trading,markets,proposals): market banners (#5494)
Co-authored-by: bwallacee <ben@vega.xyz>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-01-02 11:04:43 +00:00
Bartłomiej Głownia ad74b12908 feat(trading): do not show error message if there is no faucet tx error (#5539) 2024-01-02 10:19:10 +01:00
Matthew Russell 7fff33e493 feat(trading): competition feature flag and team page (#5549) 2023-12-29 15:50:51 +00:00
daro-maj e7cb2c8276 chore(trading): fix fees tests (#5548) 2023-12-29 15:25:33 +00:00
4adaeea40a feat(trading): pane settings view (#5509)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-12-28 12:33:18 +00:00
Matthew Russell d47d894147 fix(deal-ticket): stray curly in stop order deal ticket (#5547) 2023-12-28 12:27:56 +00:00
Bartłomiej Głownia 515932d401 fix(trading): use correct interpolation prefix in market info label (#5537) 2023-12-27 18:47:19 +00:00
daro-maj 723ff2805d chore(trading): check e2e tests issue (#5544) 2023-12-27 14:07:27 +01:00
Ben 424cced4be chore(trading): vegaservice import (#5536) 2023-12-27 08:17:20 +00:00
Daniel b2c7ee82c5 feat(ci): validators testnet deployment (#5541) 2023-12-22 18:05:31 +00:00
m.rayandDaniel 4cc63be4f9 chore(trading): update validator testnet pipelines (#5543)
Co-authored-by: Daniel <daniel.1302@gmail.com>
2023-12-22 18:03:45 +00:00
342 changed files with 28156 additions and 4548 deletions
+46 -3
View File
@@ -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: 20 timeout-minutes: 45
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: 20 timeout-minutes: 45
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 4 --dist loadfile --durations=15 run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 1 --dist loadfile --durations=45
working-directory: apps/trading/e2e working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# upload traces # upload traces
@@ -227,3 +227,46 @@ 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.skip('Proposal page displayed on mobile', function () { it('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('Able to view new asset proposal', function () { it.skip('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);
@@ -24,7 +24,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const ref = useRef<AgGridReact>(null); const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => { const showColumnsOnDesktop = () => {
ref.current?.columnApi.setColumnsVisible( ref.current?.api.setColumnsVisible(
['id', 'type', 'status'], ['id', 'type', 'status'],
window.innerWidth > BREAKPOINT_MD window.innerWidth > BREAKPOINT_MD
); );
@@ -60,7 +60,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
} }
return ( return (
<span className="whitespace-nowrap"> <span>
{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"
@@ -29,7 +29,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
const showColumnsOnDesktop = () => { const showColumnsOnDesktop = () => {
gridRef.current?.columnApi.setColumnsVisible( gridRef.current?.api.setColumnsVisible(
['id', 'state', 'asset'], ['id', 'state', 'asset'],
window.innerWidth > BREAKPOINT_MD window.innerWidth > BREAKPOINT_MD
); );
@@ -1,9 +1,5 @@
import { render, waitFor } from '@testing-library/react'; import { render, waitFor } from '@testing-library/react';
import { import { NestedDataList, sortNestedDataByChildren } from './nested-data-list';
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 = {
@@ -61,38 +57,6 @@ 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-12 lg:mb-0"> <div className="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 md:grid-cols-5 gap-x-6 mt-4"> <div className="grid grid-cols-2 md:grid-cols-5 gap-x-6 mt-4">
<div className="mb-12 md:mb-0"> <div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4"> <h2 className="text-2xl font-bold text-dark mb-0 md: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,15 +99,17 @@ const DeterministicOrderDetails = ({
</h5> </h5>
</div> </div>
<div className="mb-12 md:mb-0"> <div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2> <h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
{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=""> <div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4"> <h2 className="text-2xl font-bold text-dark mb-0 md: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">
@@ -115,8 +117,8 @@ const DeterministicOrderDetails = ({
</h5> </h5>
</div> </div>
{o.type ? ( {o.type ? (
<div className=""> <div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4"> <h2 className="text-2xl font-bold text-dark mb-0 md: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">
@@ -44,11 +44,11 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
const showColumnsOnDesktop = () => { const showColumnsOnDesktop = () => {
gridRef.current?.columnApi.setColumnsVisible( gridRef.current?.api.setColumnsVisible(
['voting', 'cDate', 'eDate', 'type'], ['voting', 'cDate', 'eDate', 'type'],
window.innerWidth > BREAKPOINT_MD window.innerWidth > BREAKPOINT_MD
); );
gridRef.current?.columnApi.setColumnWidth( gridRef.current?.api.setColumnWidth(
'actions', 'actions',
window.innerWidth > BREAKPOINT_MD ? 221 : 80 window.innerWidth > BREAKPOINT_MD ? 221 : 80
); );
@@ -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]">
<span <div
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={`Version ${signature.version}`} title={`${signature.algo}`}
> >
{signature.algo} <span>v{signature.version}</span>
</span> </div>
<div <div
className={ className={
isOpen isOpen
@@ -0,0 +1,54 @@
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>;
@@ -0,0 +1,11 @@
query ExplorerReferralCodeOwner($id: ID!) {
referralSets(id: $id) {
edges {
node {
createdAt
updatedAt
referrer
}
}
}
}
@@ -0,0 +1,95 @@
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();
});
});
});
@@ -0,0 +1,26 @@
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>
);
};
@@ -0,0 +1,93 @@
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();
});
});
@@ -0,0 +1,73 @@
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,7 +27,8 @@ export const sharedHeaderProps = {
className: 'align-top', className: 'align-top',
}; };
const Labels: Record<BlockExplorerTransactionResult['type'], string> = { // The incoming type field is usually the right thing to show. Exceptions are listed here
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',
}; };
@@ -50,7 +51,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 = Labels[txData.type] || txData.type; const type = LabelOverrides[txData.type] || txData.type;
return ( return (
<> <>
@@ -0,0 +1,47 @@
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>
);
};
@@ -0,0 +1,52 @@
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,6 +28,10 @@ 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;
@@ -121,6 +125,14 @@ 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;
} }
@@ -0,0 +1,47 @@
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>
);
};
@@ -0,0 +1,54 @@
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,6 +28,7 @@ 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'
@@ -47,45 +48,46 @@ export type FilterOption =
| 'Vote on Proposal' | 'Vote on Proposal'
| 'Withdraw'; | 'Withdraw';
// Alphabetised list of transaction types to appear at the top level export const filterOptions: Record<string, FilterOption[]> = {
export const PrimaryFilterOptions: FilterOption[] = [ 'Market Instructions': [
'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',
'Cancel Transfer Funds', 'Liquidity Provision Order',
'Delegate', 'Stop Orders Submission',
'Liquidity Provision Order', 'Stop Orders Cancellation',
'Proposal', 'Submit Order',
'Stop Orders Submission', ],
'Stop Orders Cancellation', 'Transfers and Withdrawals': [
'Submit Oracle Data', 'Transfer Funds',
'Submit Order', 'Cancel Transfer Funds',
'Transfer Funds', 'Withdraw',
'Undelegate', ],
'Vote on Proposal', Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
'Withdraw', Referrals: [
]; 'Apply Referral Code',
'Create Referral Set',
'Join Team',
'Update Referral Set',
],
'External Data': ['Chain Event', 'Submit Oracle Data'],
Validators: [
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
],
};
// Alphabetised list of transaction types to nest under a 'More...' submenu export const AllFilterOptions: FilterOption[] =
export const SecondaryFilterOptions: FilterOption[] = [ Object.values(filterOptions).flat();
'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>;
@@ -122,46 +124,33 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</> </>
)} )}
{PrimaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem {Object.entries(filterOptions).map(([key, value]) => (
key={f} <DropdownMenuSub key={key}>
checked={filters.has(f)} <DropdownMenuSubTrigger>
onCheckedChange={() => { {t(key)}
// NOTE: These act like radio buttons until the API supports multiple filters <Icon name="chevron-right" />
setFilters(new Set([f])); </DropdownMenuSubTrigger>
}} <DropdownMenuSubContent>
id={`radio-${f}`} {value.map((f) => (
> <DropdownMenuCheckboxItem
{f} key={f}
<DropdownMenuItemIndicator> checked={filters.has(f)}
<Icon name="tick-circle" /> onCheckedChange={(checked) => {
</DropdownMenuItemIndicator> // NOTE: These act like radio buttons until the API supports multiple filters
</DropdownMenuCheckboxItem> setFilters(new Set([f]));
}}
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,6 +48,8 @@ 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(
+1 -1
View File
@@ -73,7 +73,7 @@ export const Layout = () => {
<ProtocolUpgradeInProgressNotification /> <ProtocolUpgradeInProgressNotification />
</div> </div>
<div className={fixedWidthClasses}> <div className={fixedWidthClasses}>
<main className="p-4"> <main className="md:p-4">
{!isHome && <BreadcrumbsContainer className="mb-4" />} {!isHome && <BreadcrumbsContainer className="mb-4" />}
<Outlet /> <Outlet />
</main> </main>
@@ -31,6 +31,10 @@ const Tx = () => {
} }
} }
if (!data || !data?.transaction) {
errorMessage = 'Transaction not found';
}
return ( return (
<section> <section>
<PageHeader <PageHeader
@@ -49,7 +53,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) { if (!txData || !pubKey) {
return <>{t('Awaiting Block Explorer transaction details')}</>; return <>{t('Transaction could not be found')}</>;
} }
return ( return (
<section className="mb-10" key={txData.hash}> <section className="mb-10" key={txData.hash}>
+2 -2
View File
@@ -8,13 +8,13 @@ type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = T | U extends object 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('Unable to submit proposal with public key', function () { it.skip('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,7 +55,10 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.getByTestId('dialog-content') cy.getByTestId('dialog-content')
.first() .first()
.within(() => { .within(() => {
cy.get('h1').should('have.text', 'Transaction failed'); cy.getByTestId('dialog-title').should(
'have.text',
'Transaction failed'
);
cy.getByTestId('Error').should('have.text', expectedErrorTxt); cy.getByTestId('Error').should('have.text', expectedErrorTxt);
}); });
}); });
@@ -112,6 +112,7 @@ 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',
@@ -196,6 +197,7 @@ 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',
}, },
@@ -240,6 +242,7 @@ 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',
@@ -334,6 +337,7 @@ 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',
}, },
+9 -2
View File
@@ -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 } from '@vegaprotocol/wallet'; import { VegaWalletProvider, useChainId } from '@vegaprotocol/wallet';
import { import {
useVegaTransactionManager, useVegaTransactionManager,
useVegaTransactionUpdater, useVegaTransactionUpdater,
@@ -96,7 +96,9 @@ 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 = () => {
@@ -123,6 +125,9 @@ 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(
@@ -157,7 +162,8 @@ 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;
} }
@@ -169,6 +175,7 @@ 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,
+112
View File
@@ -0,0 +1,112 @@
import type { ApolloError } from '@apollo/client';
import {
PARTY_NOT_FOUND,
filterAcceptableGraphqlErrors,
isPartyNotFoundError,
} from './party';
import type { GraphQLError } from 'graphql';
/**
*
* @param message
* @returns GraphQLError
*/
function createMockApolloErrors(message: string): GraphQLError {
return {
message,
extensions: {
code: message.toUpperCase().replace(/ /g, '_'),
},
locations: [],
originalError: new Error(message),
path: [],
nodes: [],
positions: [1],
name: message,
source: {
body: message,
name: message,
locationOffset: {
line: 1,
column: 1,
},
},
};
}
describe('filterAcceptableGraphqlErrors', () => {
it('should return undefined if the error is a party not found error', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [createMockApolloErrors('failed to get party for ID')],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toBeUndefined();
});
it('should return the error if it is not a party not found error', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [createMockApolloErrors('Some other error')],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toEqual(error);
});
it('should return the error if there are multiple errors', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [
createMockApolloErrors('failed to get party for ID'),
createMockApolloErrors('Some other error'),
],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toEqual(error);
});
it('should return the error if there are no errors', () => {
const error: Partial<ApolloError> = {
graphQLErrors: [],
};
const result = filterAcceptableGraphqlErrors(error as ApolloError);
expect(result).toEqual(error);
});
it('should return undefined if the error is undefined', () => {
const result = filterAcceptableGraphqlErrors(undefined);
expect(result).toBeUndefined();
});
});
describe('isPartyNotFoundError', () => {
it('should return true if the error message includes PARTY_NOT_FOUND', () => {
const error = { message: 'failed to get party for ID' };
const result = isPartyNotFoundError(error);
expect(result).toBe(true);
});
it('should return false if the error message does not include PARTY_NOT_FOUND', () => {
const error = { message: 'Some other error' };
const result = isPartyNotFoundError(error);
expect(result).toBe(false);
});
// Will trip if the error message changes, which should not be a problem, but there
// might be logic that depends on it
it('expects party not found error to remain consistent', () => {
const error = 'failed to get party for ID';
expect(PARTY_NOT_FOUND).toStrictEqual(error);
});
});
+22
View File
@@ -1,3 +1,5 @@
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 }) => {
@@ -6,3 +8,23 @@ 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;
}
@@ -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(
'Market change: MarketId' 'Update to market ID: MarketId'
); );
}); });
@@ -1,5 +1,11 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import {
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';
@@ -12,7 +18,12 @@ import {
useNewTransferProposalDetails, useNewTransferProposalDetails,
useSuccessorMarketProposalDetails, useSuccessorMarketProposalDetails,
} from '@vegaprotocol/proposals'; } from '@vegaprotocol/proposals';
import { useFeatureFlags } from '@vegaprotocol/environment'; import {
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';
@@ -32,6 +43,8 @@ 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 = '';
@@ -106,8 +119,33 @@ export const ProposalHeader = ({
fallbackTitle = t('UpdateMarketProposal'); fallbackTitle = t('UpdateMarketProposal');
details = ( details = (
<> <>
<span>{t('MarketChange')}:</span>{' '} <span>{t('UpdateToMarket')}:</span>{' '}
<span>{truncateMiddle(change.marketId)}</span> <span className="inline-flex items-start gap-2">
<span className="break-all">{change.marketId} </span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={change.marketId}
description={t('copyToClipboard')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<button
className="inline-block px-1"
onClick={() => {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', change.marketId)
);
window.open(marketPageLink, '_blank');
}}
>
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</button>
</Tooltip>
</span>
</span>
</> </>
); );
break; break;
@@ -48,6 +48,7 @@ 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,6 +18,7 @@ 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;
@@ -42,8 +43,10 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
skip: !pubKey, skip: !pubKey,
}); });
const filteredErrors = filterAcceptableGraphqlErrors(error);
return ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <AsyncRenderer loading={loading} error={filteredErrors} data={data}>
<VoteButtons <VoteButtons
{...props} {...props}
currentStakeAvailable={toBigNum( currentStakeAvailable={toBigNum(
@@ -205,7 +205,6 @@ query Proposal(
} }
} }
liquidityMonitoringParameters { liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters { targetStakeParameters {
timeWindow timeWindow
scalingFactor scalingFactor
@@ -213,7 +212,6 @@ query Proposal(
} }
positionDecimalPlaces positionDecimalPlaces
linearSlippageFactor linearSlippageFactor
quadraticSlippageFactor
} }
... on UpdateMarket { ... on UpdateMarket {
marketId marketId
@@ -366,7 +364,6 @@ query Proposal(
} }
} }
liquidityMonitoringParameters { liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters { targetStakeParameters {
timeWindow timeWindow
scalingFactor scalingFactor
File diff suppressed because one or more lines are too long
@@ -10,6 +10,7 @@ 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;
@@ -99,17 +100,24 @@ 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={error} error={filteredErrors}
data={data} data={filteredData}
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
@@ -118,17 +126,19 @@ export const EpochIndividualRewards = ({
/> />
) )
)} )}
<Pagination {epochIndividualRewardSummaries.length > 0 && (
isLoading={loading} <Pagination
hasPrevPage={page > 1} isLoading={loading}
hasNextPage={page < totalPages} hasPrevPage={page > 1}
onBack={() => refetchData(page - 1)} hasNextPage={page < totalPages}
onNext={() => refetchData(page + 1)} onBack={() => refetchData(page - 1)}
onFirst={() => refetchData(1)} onNext={() => refetchData(page + 1)}
onLast={() => refetchData(totalPages)} onFirst={() => refetchData(1)}
> onLast={() => refetchData(totalPages)}
{t('Page')} {page} >
</Pagination> {t('Page')} {page}
</Pagination>
)}
</div> </div>
)} )}
/> />
@@ -16,7 +16,7 @@ import type { ValidatorsView } from './validator-tables';
const nodeFactory = ( const nodeFactory = (
overrides?: PartialDeep<NodesFragmentFragment> overrides?: PartialDeep<NodesFragmentFragment>
): NodesFragmentFragment => { ): NodesFragmentFragment => {
const defaultNode = { const defaultNode: NodesFragmentFragment = {
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('10.07%'); ).toHaveTextContent('13.16%');
expect( expect(
grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]') grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]')
@@ -185,6 +185,15 @@ 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,
@@ -213,11 +222,11 @@ export const ConsensusValidatorsTable = ({
2 2
), ),
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage( [ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
calculateOverstakedPenalty(id, allNodesInPreviousEpoch), overstakingPenalty,
2 2
), ),
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage( [ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
calculateOverallPenalty(id, allNodesInPreviousEpoch), totalPenalty,
2 2
), ),
[ValidatorFields.PENDING_STAKE]: pendingStake, [ValidatorFields.PENDING_STAKE]: pendingStake,
@@ -124,6 +124,15 @@ 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,
@@ -154,11 +163,11 @@ export const StandbyPendingValidatorsTable = ({
2 2
), ),
[ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage( [ValidatorFields.OVERSTAKING_PENALTY]: formatNumberPercentage(
calculateOverstakedPenalty(id, allNodesInPreviousEpoch), overstakingPenalty,
2 2
), ),
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage( [ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
calculateOverallPenalty(id, allNodesInPreviousEpoch), totalPenalty,
2 2
), ),
[ValidatorFields.PENDING_STAKE]: pendingStake, [ValidatorFields.PENDING_STAKE]: pendingStake,
@@ -266,7 +266,9 @@ export const ValidatorTable = ({
<Tooltip description={t('OverstakedPenaltyDescription')}> <Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty"> <span data-testid="overstaking-penalty">
{formatNumberPercentage(penalties.overstaked, 2)} {penalties.overstaked
? formatNumberPercentage(penalties.overstaked, 2)
: '-'}
</span> </span>
</Tooltip> </Tooltip>
</KeyValueTableRow> </KeyValueTableRow>
@@ -285,7 +287,9 @@ export const ValidatorTable = ({
</span> </span>
<span data-testid="total-penalties"> <span data-testid="total-penalties">
<strong> <strong>
{formatNumberPercentage(penalties.overall, 2)} {penalties.overall
? formatNumberPercentage(penalties.overall, 2)
: '-'}
</strong> </strong>
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
+106 -45
View File
@@ -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,38 +106,6 @@ 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));
@@ -152,17 +120,6 @@ 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(
@@ -182,3 +139,107 @@ 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();
});
});
+35 -61
View File
@@ -5,6 +5,7 @@ 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<
@@ -21,7 +22,10 @@ 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 = (nodeId: string, nodes: Node[]) => { const calculateTheoreticalStakeScore = (
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);
@@ -42,14 +46,25 @@ const calculateTheoreticalStakeScore = (nodeId: string, nodes: Node[]) => {
* @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 = (nodeId: string, nodes: Node[]) => { export const calculateOverallPenalty = (
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 (!node || tts.isZero()) { if (
!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 || 0).dividedBy(tts)) .minus(new BigNumber(node.rewardScore.validatorScore).dividedBy(tts))
.times(100); .times(100);
return penalty.isLessThan(0) ? new BigNumber(0) : penalty; return penalty.isLessThan(0) ? new BigNumber(0) : penalty;
}; };
@@ -60,10 +75,21 @@ export const calculateOverallPenalty = (nodeId: string, nodes: Node[]) => {
* @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 = (nodeId: string, nodes: Node[]) => { export const calculateOverstakedPenalty = (
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 (!node || tts.isZero()) { if (
!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)
@@ -78,7 +104,9 @@ export const calculateOverstakedPenalty = (nodeId: string, nodes: Node[]) => {
* Calculates performance penalty based on the given performance score. * Calculates performance penalty based on the given performance score.
* @returns % * @returns %
*/ */
export const calculatesPerformancePenalty = (performanceScore: string) => { export const calculatesPerformancePenalty = (
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);
@@ -123,60 +151,6 @@ 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%'
+37 -1
View File
@@ -1,14 +1,50 @@
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit'; import {
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} />;
+8 -1
View File
@@ -21,7 +21,14 @@ 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_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=
+1
View File
@@ -21,6 +21,7 @@ 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
+1
View File
@@ -20,6 +20,7 @@ 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
+4
View File
@@ -21,6 +21,7 @@ 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
@@ -28,3 +29,6 @@ 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=
+1
View File
@@ -21,6 +21,7 @@ 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
+1
View File
@@ -21,6 +21,7 @@ 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
+1
View File
@@ -22,6 +22,7 @@ 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
+1
View File
@@ -22,6 +22,7 @@ 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
+2
View File
@@ -54,3 +54,5 @@ 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).
+8 -13
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react'; import { useEffect } 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,17 +72,6 @@ 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>
@@ -118,6 +107,8 @@ export const MarketPage = () => {
); );
} }
const pinnedAsset = data && getAsset(data);
return ( return (
<> <>
<TitleUpdater <TitleUpdater
@@ -125,7 +116,11 @@ export const MarketPage = () => {
marketName={data?.tradableInstrument.instrument.name} marketName={data?.tradableInstrument.instrument.name}
decimalPlaces={data?.decimalPlaces} decimalPlaces={data?.decimalPlaces}
/> />
{tradeView} {largeScreen ? (
<TradeGrid market={data} pinnedAsset={pinnedAsset} />
) : (
<TradePanels market={data} pinnedAsset={pinnedAsset} />
)}
</> </>
); );
}; };
+51 -44
View File
@@ -1,11 +1,10 @@
import { memo } from 'react'; import { memo, type ReactNode } 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 type { PinnedAsset } from '@vegaprotocol/accounts'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { OracleBanner, useMarket } from '@vegaprotocol/markets'; import { type PinnedAsset } from '@vegaprotocol/accounts';
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,
@@ -13,31 +12,19 @@ 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 | null; market: Market;
pinnedAsset?: PinnedAsset; pinnedAsset?: PinnedAsset | undefined;
} }
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',
@@ -65,17 +52,17 @@ const MainGrid = memo(
menu={<TradingViews.chart.menu />} menu={<TradingViews.chart.menu />}
> >
<ErrorBoundary feature="chart"> <ErrorBoundary feature="chart">
<TradingViews.chart.component marketId={marketId} /> <TradingViews.chart.component marketId={market.id} />
</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={marketId} /> <TradingViews.depth.component marketId={market.id} />
</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={marketId} /> <TradingViews.liquidity.component marketId={market.id} />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
{market && {market &&
@@ -83,17 +70,21 @@ 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={marketId} /> <TradingViews.funding.component marketId={market.id} />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
) : null} ) : null}
{market && {market &&
market.tradableInstrument.instrument.product.__typename === market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? ( 'Perpetual' ? (
<Tab id="funding-payments" name={t('Funding payments')}> <Tab
id="funding-payments"
name={t('Funding payments')}
settings={<TradingViews.fundingPayments.settings />}
>
<ErrorBoundary feature="funding-payments"> <ErrorBoundary feature="funding-payments">
<TradingViews.fundingPayments.component <TradingViews.fundingPayments.component
marketId={marketId} marketId={market.id}
/> />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
@@ -109,12 +100,16 @@ 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={marketId} /> <TradingViews.orderbook.component marketId={market.id} />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
<Tab id="trades" name={t('Trades')}> <Tab
id="trades"
name={t('Trades')}
settings={<TradingViews.trades.settings />}
>
<ErrorBoundary feature="trades"> <ErrorBoundary feature="trades">
<TradingViews.trades.component marketId={marketId} /> <TradingViews.trades.component marketId={market.id} />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
</Tabs> </Tabs>
@@ -133,6 +128,7 @@ const MainGrid = memo(
id="positions" id="positions"
name={t('Positions')} name={t('Positions')}
menu={<TradingViews.positions.menu />} menu={<TradingViews.positions.menu />}
settings={<TradingViews.positions.settings />}
> >
<ErrorBoundary feature="positions"> <ErrorBoundary feature="positions">
<TradingViews.positions.component /> <TradingViews.positions.component />
@@ -142,17 +138,26 @@ const MainGrid = memo(
id="open-orders" id="open-orders"
name={t('Open')} name={t('Open')}
menu={<TradingViews.activeOrders.menu />} menu={<TradingViews.activeOrders.menu />}
settings={<TradingViews.activeOrders.settings />}
> >
<ErrorBoundary feature="activeOrders"> <ErrorBoundary feature="activeOrders">
<TradingViews.activeOrders.component /> <TradingViews.activeOrders.component />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
<Tab id="closed-orders" name={t('Closed')}> <Tab
id="closed-orders"
name={t('Closed')}
settings={<TradingViews.closedOrders.settings />}
>
<ErrorBoundary feature="closedOrders"> <ErrorBoundary feature="closedOrders">
<TradingViews.closedOrders.component /> <TradingViews.closedOrders.component />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
<Tab id="rejected-orders" name={t('Rejected')}> <Tab
id="rejected-orders"
name={t('Rejected')}
settings={<TradingViews.rejectedOrders.settings />}
>
<ErrorBoundary feature="rejectedOrders"> <ErrorBoundary feature="rejectedOrders">
<TradingViews.rejectedOrders.component /> <TradingViews.rejectedOrders.component />
</ErrorBoundary> </ErrorBoundary>
@@ -161,25 +166,35 @@ const MainGrid = memo(
id="orders" id="orders"
name={t('All')} name={t('All')}
menu={<TradingViews.orders.menu />} menu={<TradingViews.orders.menu />}
settings={<TradingViews.orders.settings />}
> >
<ErrorBoundary feature="orders"> <ErrorBoundary feature="orders">
<TradingViews.orders.component /> <TradingViews.orders.component />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
{featureFlags.STOP_ORDERS ? ( {featureFlags.STOP_ORDERS ? (
<Tab id="stop-orders" name={t('Stop orders')}> <Tab
id="stop-orders"
name={t('Stop orders')}
settings={<TradingViews.stopOrders.settings />}
>
<ErrorBoundary feature="stop-orders"> <ErrorBoundary feature="stop-orders">
<TradingViews.stopOrders.component /> <TradingViews.stopOrders.component />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
) : null} ) : null}
<Tab id="fills" name={t('Fills')}> <Tab
id="fills"
name={t('Fills')}
settings={<TradingViews.fills.settings />}
>
<TradingViews.fills.component /> <TradingViews.fills.component />
</Tab> </Tab>
<Tab <Tab
id="accounts" id="accounts"
name={t('Collateral')} name={t('Collateral')}
menu={<TradingViews.collateral.menu />} menu={<TradingViews.collateral.menu />}
settings={<TradingViews.collateral.settings />}
> >
<ErrorBoundary feature="collateral"> <ErrorBoundary feature="collateral">
<TradingViews.collateral.component <TradingViews.collateral.component
@@ -197,7 +212,6 @@ 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]'
@@ -206,17 +220,10 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
return ( return (
<div className={wrapperClasses}> <div className={wrapperClasses}>
<div> <div>
{featureFlags.SUCCESSOR_MARKETS && ( <MarketBanner market={market} />
<>
<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 marketId={market?.id || ''} pinnedAsset={pinnedAsset} /> <MainGrid market={market} pinnedAsset={pinnedAsset} />
</div> </div>
</div> </div>
); );
@@ -1,29 +1,30 @@
import { type PinnedAsset } from '@vegaprotocol/accounts'; import { type PinnedAsset } from '@vegaprotocol/accounts';
import { type Market } from '@vegaprotocol/markets'; import { type Market } from '@vegaprotocol/markets';
import { OracleBanner } from '@vegaprotocol/markets'; // TODO: handle oracle banner
// 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';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import { import {
MarketSuccessorBanner, Popover,
MarketSuccessorProposalBanner, Splash,
MarketTerminationBanner, VegaIcon,
} from '../../components/market-banner'; VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import { MarketBanner } 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 | null; market: Market;
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 renderView = () => { const renderView = () => {
const Component = TradingViews[view].component; const Component = TradingViews[view].component;
@@ -44,35 +45,36 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
}; };
const renderMenu = () => { const renderMenu = () => {
const viewCfg = TradingViews[view]; if ('menu' in viewCfg || 'settings' in viewCfg) {
if ('menu' in viewCfg) {
const Menu = viewCfg.menu;
return ( return (
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default"> <div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu /> {'menu' in viewCfg ? <viewCfg.menu /> : null}
{'settings' in viewCfg ? (
<Popover
align="end"
trigger={
<span className="ml-1 flex items-center justify-center h-6 w-6">
<VegaIcon name={VegaIconNames.COG} size={16} />
</span>
}
>
<div className="p-4 flex justify-end">
<viewCfg.settings />
</div>
</Popover>
) : null}
</div> </div>
); );
} }
return null;
}; };
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>
{featureFlags.SUCCESSOR_MARKETS && ( <MarketBanner market={market} />
<>
<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"> <div className="h-full relative">
<AutoSizer> <AutoSizer>
{({ width, height }) => ( {({ width, height }) => (
<div style={{ width, height }} className="overflow-auto"> <div style={{ width, height }} className="overflow-auto">
@@ -110,7 +112,9 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
key={key} key={key}
view={key} view={key}
isActive={isActive} isActive={isActive}
onClick={() => setView(key)} onClick={() => {
setView(key);
}}
/> />
); );
})} })}
@@ -1,15 +1,36 @@
import { DepthChartContainer } from '@vegaprotocol/market-depth'; import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders'; import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
import { TradesContainer } from '../../components/trades-container'; import {
TradesContainer,
TradesSettings,
} from '../../components/trades-container';
import { OrderbookContainer } from '../../components/orderbook-container'; import { OrderbookContainer } from '../../components/orderbook-container';
import { FillsContainer } from '../../components/fills-container'; import {
import { PositionsContainer } from '../../components/positions-container'; FillsContainer,
import { AccountsContainer } from '../../components/accounts-container'; FillsSettings,
} from '../../components/fills-container';
import {
PositionsContainer,
PositionsSettings,
} from '../../components/positions-container';
import {
AccountsContainer,
AccountsSettings,
} from '../../components/accounts-container';
import { LiquidityContainer } from '../../components/liquidity-container'; import { LiquidityContainer } from '../../components/liquidity-container';
import { FundingContainer } from '../../components/funding-container'; import { FundingContainer } from '../../components/funding-container';
import { FundingPaymentsContainer } from '../../components/funding-payments-container'; import {
import { OrdersContainer } from '../../components/orders-container'; FundingPaymentsContainer,
import { StopOrdersContainer } from '../../components/stop-orders-container'; FundingPaymentsSettings,
} from '../../components/funding-payments-container';
import {
OrdersContainer,
OrdersSettings,
} from '../../components/orders-container';
import {
StopOrdersContainer,
StopOrdersSettings,
} from '../../components/stop-orders-container';
import { AccountsMenu } from '../../components/accounts-menu'; import { AccountsMenu } from '../../components/accounts-menu';
import { PositionsMenu } from '../../components/positions-menu'; import { PositionsMenu } from '../../components/positions-menu';
import { ChartContainer, ChartMenu } from '../../components/chart-container'; import { ChartContainer, ChartMenu } from '../../components/chart-container';
@@ -32,37 +53,46 @@ export const TradingViews = {
}, },
fundingPayments: { fundingPayments: {
component: FundingPaymentsContainer, component: FundingPaymentsContainer,
settings: FundingPaymentsSettings,
}, },
orderbook: { orderbook: {
component: OrderbookContainer, component: OrderbookContainer,
}, },
trades: { trades: {
component: TradesContainer, component: TradesContainer,
settings: TradesSettings,
}, },
positions: { positions: {
component: PositionsContainer, component: PositionsContainer,
menu: PositionsMenu, menu: PositionsMenu,
settings: PositionsSettings,
}, },
activeOrders: { activeOrders: {
component: () => <OrdersContainer filter={Filter.Open} />, component: () => <OrdersContainer filter={Filter.Open} />,
menu: OpenOrdersMenu, menu: OpenOrdersMenu,
settings: () => <OrdersSettings filter={Filter.Open} />,
}, },
closedOrders: { closedOrders: {
component: () => <OrdersContainer filter={Filter.Closed} />, component: () => <OrdersContainer filter={Filter.Closed} />,
settings: () => <OrdersSettings filter={Filter.Closed} />,
}, },
rejectedOrders: { rejectedOrders: {
component: () => <OrdersContainer filter={Filter.Rejected} />, component: () => <OrdersContainer filter={Filter.Rejected} />,
settings: () => <OrdersSettings filter={Filter.Rejected} />,
}, },
orders: { orders: {
component: OrdersContainer, component: OrdersContainer,
menu: OpenOrdersMenu, menu: OpenOrdersMenu,
settings: OrdersSettings,
}, },
stopOrders: { stopOrders: {
component: StopOrdersContainer, component: StopOrdersContainer,
settings: StopOrdersSettings,
}, },
collateral: { collateral: {
component: AccountsContainer, component: AccountsContainer,
menu: AccountsMenu, menu: AccountsMenu,
settings: AccountsSettings,
}, },
fills: { component: FillsContainer }, fills: { component: FillsContainer, settings: FillsSettings },
} as const; } as const;
@@ -314,7 +314,7 @@ describe('Closed', () => {
}); });
it('display market actions', async () => { it('display market actions', async () => {
// Use market with a succcessor Id as the actions dropdown will optionally // Use market with a successor 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,6 +137,8 @@ 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'),
@@ -280,6 +282,7 @@ 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,6 +17,7 @@ const defaultColDef = {
filter: true, filter: true,
resizable: true, resizable: true,
filterParams: { buttons: ['reset'] }, filterParams: { buttons: ['reset'] },
minWidth: 120,
}; };
const components = { const components = {
@@ -42,7 +43,7 @@ export const createDataGridSlice: StateCreator<DataGridSlice> = (set) => ({
}, },
}); });
const useMarketsStore = create<DataGridSlice>()( export const useMarketsStore = create<DataGridSlice>()(
persist(createDataGridSlice, { persist(createDataGridSlice, {
name: 'vega_market_list_store', name: 'vega_market_list_store',
}) })
@@ -16,6 +16,7 @@ import {
} from '@vegaprotocol/environment'; } 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 { MarketsSettings } from './markets-settings';
export const MarketsPage = () => { export const MarketsPage = () => {
const t = useT(); const t = useT();
@@ -34,7 +35,11 @@ export const MarketsPage = () => {
<div className="h-full pt-0.5 pb-3 px-1.5"> <div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border rounded-sm border-default"> <div className="h-full my-1 border rounded-sm border-default">
<Tabs storageKey="console-markets"> <Tabs storageKey="console-markets">
<Tab id="open-markets" name={t('Open markets')}> <Tab
id="open-markets"
name={t('Open markets')}
settings={<MarketsSettings />}
>
<ErrorBoundary feature="markets-open"> <ErrorBoundary feature="markets-open">
<OpenMarkets /> <OpenMarkets />
</ErrorBoundary> </ErrorBoundary>
@@ -42,11 +47,13 @@ export const MarketsPage = () => {
<Tab <Tab
id="proposed-markets" id="proposed-markets"
name={t('Proposed markets')} name={t('Proposed markets')}
settings={<MarketsSettings />}
menu={ menu={
<TradingAnchorButton <TradingAnchorButton
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>
@@ -56,7 +63,11 @@ export const MarketsPage = () => {
<Proposed /> <Proposed />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
<Tab id="closed-markets" name={t('Closed markets')}> <Tab
id="closed-markets"
name={t('Closed markets')}
settings={<MarketsSettings />}
>
<ErrorBoundary feature="markets-closed"> <ErrorBoundary feature="markets-closed">
<Closed /> <Closed />
</ErrorBoundary> </ErrorBoundary>
@@ -0,0 +1,8 @@
import { GridSettings } from '../../components/grid-settings/grid-settings';
import { useMarketsStore } from './market-list-table';
export const MarketsSettings = () => (
<GridSettings
updateGridStore={useMarketsStore((store) => store.updateGridStore)}
/>
);
@@ -29,6 +29,7 @@ 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,
@@ -5,14 +5,29 @@ import { titlefy } from '@vegaprotocol/utils';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws'; import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit'; import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { usePageTitleStore } from '../../stores'; import { usePageTitleStore } from '../../stores';
import { AccountsContainer } from '../../components/accounts-container'; import {
AccountsContainer,
AccountsSettings,
} from '../../components/accounts-container';
import { DepositsContainer } from '../../components/deposits-container'; import { DepositsContainer } from '../../components/deposits-container';
import { FillsContainer } from '../../components/fills-container'; import {
import { FundingPaymentsContainer } from '../../components/funding-payments-container'; FillsContainer,
import { PositionsContainer } from '../../components/positions-container'; FillsSettings,
} from '../../components/fills-container';
import {
FundingPaymentsContainer,
FundingPaymentsSettings,
} from '../../components/funding-payments-container';
import {
PositionsContainer,
PositionsSettings,
} from '../../components/positions-container';
import { PositionsMenu } from '../../components/positions-menu'; import { PositionsMenu } from '../../components/positions-menu';
import { WithdrawalsContainer } from '../../components/withdrawals-container'; import { WithdrawalsContainer } from '../../components/withdrawals-container';
import { OrdersContainer } from '../../components/orders-container'; import {
OrdersContainer,
OrdersSettings,
} from '../../components/orders-container';
import { LedgerContainer } from '../../components/ledger-container'; import { LedgerContainer } from '../../components/ledger-container';
import { import {
ResizableGrid, ResizableGrid,
@@ -76,22 +91,27 @@ export const Portfolio = () => {
id="positions" id="positions"
name={t('Positions')} name={t('Positions')}
menu={<PositionsMenu />} menu={<PositionsMenu />}
settings={<PositionsSettings />}
> >
<ErrorBoundary feature="portfolio-positions"> <ErrorBoundary feature="portfolio-positions">
<PositionsContainer allKeys /> <PositionsContainer allKeys />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
<Tab id="orders" name={t('Orders')}> <Tab id="orders" name={t('Orders')} settings={<OrdersSettings />}>
<ErrorBoundary feature="portfolio-orders"> <ErrorBoundary feature="portfolio-orders">
<OrdersContainer /> <OrdersContainer />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')} settings={<FillsSettings />}>
<ErrorBoundary feature="portfolio-fills"> <ErrorBoundary feature="portfolio-fills">
<FillsContainer /> <FillsContainer />
</ErrorBoundary> </ErrorBoundary>
</Tab> </Tab>
<Tab id="funding-payments" name={t('Funding payments')}> <Tab
id="funding-payments"
name={t('Funding payments')}
settings={<FundingPaymentsSettings />}
>
<ErrorBoundary feature="portfolio-funding-payments"> <ErrorBoundary feature="portfolio-funding-payments">
<FundingPaymentsContainer /> <FundingPaymentsContainer />
</ErrorBoundary> </ErrorBoundary>
@@ -114,6 +134,7 @@ export const Portfolio = () => {
<Tab <Tab
id="collateral" id="collateral"
name={t('Collateral')} name={t('Collateral')}
settings={<AccountsSettings />}
menu={<AccountsMenu />} menu={<AccountsMenu />}
> >
<ErrorBoundary feature="portfolio-accounts"> <ErrorBoundary feature="portfolio-accounts">
@@ -296,7 +296,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
<> <>
<div <div
data-testid="referral-apply-code-form" data-testid="referral-apply-code-form"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8" className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto md:w-2/3 max-w-md rounded-lg p-8"
> >
<h3 className="calt mb-4 text-center text-2xl"> <h3 className="calt mb-4 text-center text-2xl">
{t('Apply a referral code')} {t('Apply a referral code')}
@@ -3,7 +3,7 @@ export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent'; 'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const SKY_BACKGROUND = export const SKY_BACKGROUND =
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local'; 'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
// TODO: Update the links to use the correct referral related pages // TODO: Update the links to use the correct referral related pages
export const REFERRAL_DOCS_LINK = export const REFERRAL_DOCS_LINK =
@@ -49,7 +49,7 @@ export const CreateCodeForm = () => {
return ( return (
<div <div
data-testid="referral-create-code-form" data-testid="referral-create-code-form"
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg" className="md:w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
> >
<h3 className="mb-4 text-2xl text-center calt"> <h3 className="mb-4 text-2xl text-center calt">
{t('Create a referral code')} {t('Create a referral code')}
@@ -1,5 +1,5 @@
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { Table } from './table'; import { Table } from '../../components/table';
export const HowItWorksTable = () => { export const HowItWorksTable = () => {
const t = useT(); const t = useT();
@@ -5,16 +5,16 @@ import { useT } from '../../lib/use-t';
export const LandingBanner = () => { export const LandingBanner = () => {
const t = useT(); const t = useT();
return ( return (
<div className={classNames('relative mb-20')}> <div className={classNames('relative mb-10 lg:mb-20')}>
<div className=""> <div className="">
<div <div
aria-hidden aria-hidden
className="absolute top-20 right-[120px] md:right-[240px] max-sm:hidden" className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
> >
<AnimatedDudeWithWire /> <AnimatedDudeWithWire />
</div> </div>
<div className="pt-20 sm:w-[50%]"> <div className="pt-10 lg:pt-20 sm:w-[50%]">
<h1 className="text-6xl font-alpha calt mb-10"> <h1 className="text-3xl _text-[6vw] lg:!text-6xl leading-[1em] font-alpha calt mb-10">
{t('Vega community referrals')} {t('Vega community referrals')}
</h1> </h1>
<p className="text-lg mb-1"> <p className="text-lg mb-1">
@@ -22,7 +22,7 @@ export const LandingBanner = () => {
'Referral programs can be proposed and created via community governance.' 'Referral programs can be proposed and created via community governance.'
)} )}
</p> </p>
<p className="text-lg mb-10"> <p className="text-lg mb-1">
{t( {t(
'Once live, users can generate referral codes to share with their friends and earn commission on their trades, while referred traders can access fee discounts based on the running volume of the group.' 'Once live, users can generate referral codes to share with their friends and earn commission on their trades, while referred traders can access fee discounts based on the running volume of the group.'
)} )}
@@ -13,7 +13,7 @@ export const Layout = ({
<div <div
className={classNames( className={classNames(
'max-w-[1440px]', 'max-w-[1440px]',
'mx-auto px-16 md:px-32 pb-32', 'mx-auto px-4 lg:px-32 pb-32',
'relative z-0', 'relative z-0',
className className
)} )}
@@ -14,7 +14,7 @@ import {
useUpdateReferees, useUpdateReferees,
} from './hooks/use-referral'; } from './hooks/use-referral';
import classNames from 'classnames'; import classNames from 'classnames';
import { Table } from './table'; import { Table } from '../../components/table';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
getDateFormat, getDateFormat,
@@ -72,7 +72,7 @@ export const Referrals = () => {
{showNav && <Nav />} {showNav && <Nav />}
<div <div
className={classNames({ className={classNames({
'py-16': showNav, 'py-8 lg:py-16': showNav,
'h-[300px] relative': loading || error, 'h-[300px] relative': loading || error,
})} })}
> >
+24 -41
View File
@@ -3,11 +3,12 @@ import {
getDateTimeFormat, getDateTimeFormat,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { useReferralProgram } from './hooks/use-referral-program'; import { useReferralProgram } from './hooks/use-referral-program';
import { Table } from './table'; import { Table } from '../../components/table';
import classNames from 'classnames'; import classNames from 'classnames';
import { BORDER_COLOR, GRADIENT } from './constants'; import { BORDER_COLOR, GRADIENT } from './constants';
import { Tag } from './tag'; import { Tag } from '../../components/helpers/tag';
import type { ComponentProps, ReactNode } from 'react'; import { getTierColor, getTierGradient } from '../../components/helpers/tiers';
import type { ReactNode } from 'react';
import { ExternalLink, truncateMiddle } from '@vegaprotocol/ui-toolkit'; import { ExternalLink, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { import {
DApp, DApp,
@@ -19,25 +20,6 @@ import {
import { useT, ns } from '../../lib/use-t'; import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
// rainbow-ish order
const TIER_COLORS: Array<ComponentProps<typeof Tag>['color']> = [
'pink',
'orange',
'yellow',
'green',
'blue',
'purple',
];
const getTierColor = (tier: number) => {
const tiers = Object.keys(TIER_COLORS).length;
let index = Math.abs(tier - 1);
if (tier >= tiers) {
index = index % tiers;
}
return TIER_COLORS[index];
};
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => ( const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
<div <div
className={classNames( className={classNames(
@@ -53,10 +35,12 @@ const StakingTier = ({
tier, tier,
referralRewardMultiplier, referralRewardMultiplier,
minimumStakedTokens, minimumStakedTokens,
max,
}: { }: {
tier: number; tier: number;
referralRewardMultiplier: string; referralRewardMultiplier: string;
minimumStakedTokens: string; minimumStakedTokens: string;
max?: number;
}) => { }) => {
const t = useT(); const t = useT();
const minimum = addDecimalsFormatNumber(minimumStakedTokens, 18); const minimum = addDecimalsFormatNumber(minimumStakedTokens, 18);
@@ -97,7 +81,7 @@ const StakingTier = ({
)} )}
> >
<div> <div>
<Tag color={getTierColor(tier)}> <Tag color={getTierColor(tier, max)}>
{t('Multiplier')} {referralRewardMultiplier}x {t('Multiplier')} {referralRewardMultiplier}x
</Tag> </Tag>
<p className="mt-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100"> <p className="mt-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
@@ -159,7 +143,9 @@ export const TiersContainer = () => {
return ( return (
<> <>
<h2 className="text-3xl mt-10">{t('Current program details')}</h2> <h2 className="text-3xl mt-10 font-alpha calt">
{t('Current program details')}
</h2>
{details?.id && ( {details?.id && (
<p> <p>
<Trans <Trans
@@ -198,7 +184,18 @@ export const TiersContainer = () => {
</div> </div>
{/* Container */} {/* Container */}
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20"> <div
className={classNames(
'md:bg-vega-clight-800',
'md:dark:bg-vega-cdark-800',
'md:text-black',
'md:dark:text-white',
'md:rounded-lg',
'md:p-6',
'mt-1',
'mb-20'
)}
>
{/* Benefit tiers */} {/* Benefit tiers */}
<div className="flex flex-col mb-5"> <div className="flex flex-col mb-5">
<h3 className="text-2xl calt">{t('Benefit tiers')}</h3> <h3 className="text-2xl calt">{t('Benefit tiers')}</h3>
@@ -261,6 +258,7 @@ const StakingTiers = ({
<StakingTier <StakingTier
key={i} key={i}
tier={tier} tier={tier}
max={data.length}
referralRewardMultiplier={referralRewardMultiplier} referralRewardMultiplier={referralRewardMultiplier}
minimumStakedTokens={minimumStakedTokens} minimumStakedTokens={minimumStakedTokens}
/> />
@@ -322,22 +320,7 @@ const TiersTable = ({
className="bg-white dark:bg-vega-cdark-900" className="bg-white dark:bg-vega-cdark-900"
data={data.map((d) => ({ data={data.map((d) => ({
...d, ...d,
className: classNames({ className: classNames(getTierGradient(d.tier, data.length)),
'from-vega-yellow-400 dark:from-vega-yellow-600 to-20% bg-highlight':
'yellow' === getTierColor(d.tier),
'from-vega-green-400 dark:from-vega-green-600 to-20% bg-highlight':
'green' === getTierColor(d.tier),
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
'blue' === getTierColor(d.tier),
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
'purple' === getTierColor(d.tier),
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
'pink' === getTierColor(d.tier),
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
'orange' === getTierColor(d.tier),
'from-vega-clight-200 dark:from-vega-cdark-200 to-20% bg-highlight':
'none' === getTierColor(d.tier),
}),
}))} }))}
/> />
); );
+10 -5
View File
@@ -19,9 +19,11 @@ export const Tile = ({
return ( return (
<div <div
className={classNames( className={classNames(
'rounded-lg overflow-hidden relative', 'text-black dark:text-white',
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white', 'overflow-hidden relative',
'p-6', 'p-3 md:p-6',
'rounded-lg',
'bg-vega-clight-800 dark:bg-vega-cdark-800',
className className
)} )}
> >
@@ -55,7 +57,10 @@ export const StatTile = ({
> >
{title} {title}
</h3> </h3>
<div data-testid={`${testId}-value`} className="text-5xl text-left"> <div
data-testid={`${testId}-value`}
className="text-2xl lg:text-5xl text-left"
>
{children} {children}
</div> </div>
{description && ( {description && (
@@ -119,7 +124,7 @@ export const CodeTile = ({
> >
<div <div
className={classNames( className={classNames(
'relative bg-rainbow bg-clip-text text-transparent text-5xl overflow-hidden', 'relative bg-rainbow bg-clip-text text-transparent text-2xl lg:text-5xl overflow-hidden',
FADE_OUT_STYLE FADE_OUT_STYLE
)} )}
> >
@@ -4,6 +4,7 @@ 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();
@@ -16,10 +17,10 @@ export const Rewards = () => {
}, [updateTitle, title]); }, [updateTitle, title]);
return ( return (
<ErrorBoundary feature="rewards"> <ErrorBoundary feature="rewards">
<div className="container mx-auto p-4"> <TinyScroll className="p-4 max-h-full overflow-auto">
<h1 className="px-4 pb-4 text-2xl">{title}</h1> <h1 className="px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer /> <RewardsContainer />
</div> </TinyScroll>
</ErrorBoundary> </ErrorBoundary>
); );
}; };
+1
View File
@@ -0,0 +1 @@
export { Teams } from './teams';
@@ -0,0 +1,7 @@
export const Teams = () => {
return (
<div>
<h1>Teams</h1>
</div>
);
};
@@ -73,7 +73,7 @@ export const AccountsContainer = ({
); );
}; };
const useAccountStore = create<DataGridSlice>()( export const useAccountStore = create<DataGridSlice>()(
persist(createDataGridSlice, { persist(createDataGridSlice, {
name: 'vega_accounts_store', name: 'vega_accounts_store',
}) })
@@ -0,0 +1,8 @@
import { GridSettings } from '../grid-settings/grid-settings';
import { useAccountStore } from './accounts-container';
export const AccountsSettings = () => (
<GridSettings
updateGridStore={useAccountStore((store) => store.updateGridStore)}
/>
);
@@ -1 +1,2 @@
export * from './accounts-container'; export * from './accounts-container';
export * from './accounts-settings';
@@ -12,6 +12,8 @@ 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();
@@ -25,13 +27,16 @@ 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 />;
} }
@@ -52,28 +57,39 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
/> />
} }
> >
<Web3Provider <DataLoader
skeleton={<AppLoader />} skeleton={<AppLoader />}
failure={ failure={
<AppFailure title={t('Could not configure web3 provider')} /> <AppFailure
title={t('Could not load market data or asset data')}
error={error}
/>
} }
> >
<VegaWalletProvider <Web3Provider
config={{ skeleton={<AppLoader />}
network: VEGA_ENV, failure={
vegaUrl: VEGA_URL, <AppFailure title={t('Could not configure web3 provider')} />
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
</VegaWalletProvider> config={{
</Web3Provider> network: VEGA_ENV,
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>
); );
@@ -0,0 +1,34 @@
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}</>;
};
+17 -3
View File
@@ -9,6 +9,7 @@ export const Card = ({
loading = false, loading = false,
highlight = false, highlight = false,
testId, testId,
noBackgroundOnMobile = false,
}: { }: {
children: ReactNode; children: ReactNode;
title: string; title: string;
@@ -16,20 +17,33 @@ export const Card = ({
loading?: boolean; loading?: boolean;
highlight?: boolean; highlight?: boolean;
testId?: string; testId?: string;
noBackgroundOnMobile?: boolean;
}) => { }) => {
return ( return (
<div <div
data-testid={testId} data-testid={testId}
className={classNames( className={classNames(
'bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full p-0.5 lg:col-auto', 'col-span-full lg:col-auto',
'rounded-lg', {
'rounded-lg bg-vega-clight-800 dark:bg-vega-cdark-800 p-0.5':
!noBackgroundOnMobile,
'mt-3 md:mt-0 md:rounded-lg md:bg-vega-clight-800 md:dark:bg-vega-cdark-800 md:p-0.5':
noBackgroundOnMobile,
},
{ {
'bg-rainbow': highlight, 'bg-rainbow': highlight,
}, },
className className
)} )}
> >
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4"> <div
className={classNames('h-full w-full', {
'bg-vega-clight-800 dark:bg-vega-cdark-800 rounded p-4':
!noBackgroundOnMobile,
'md:bg-vega-clight-800 md:dark:bg-vega-cdark-800 md:rounded md:p-4':
noBackgroundOnMobile,
})}
>
<h2 className="mb-3">{title}</h2> <h2 className="mb-3">{title}</h2>
{loading ? <CardLoader /> : children} {loading ? <CardLoader /> : children}
</div> </div>
@@ -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,
setTradingViewStudies, state,
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: { studies: string[] }) => { onAutoSaveNeeded={(data) => {
setTradingViewStudies(data.studies); setState(data);
}} }}
state={state}
/> />
); );
} }
@@ -27,10 +27,10 @@ describe('ChartMenu', () => {
render(<ChartMenu />); render(<ChartMenu />);
await userEvent.click(screen.getByRole('button', { name: 'TradingView' })); await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview'); expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
await userEvent.click(screen.getByRole('button', { name: 'Vega chart' })); await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant'); expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
}); });
@@ -68,6 +68,7 @@ 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,6 +9,7 @@ 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
@@ -17,7 +18,6 @@ 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;
setTradingViewStudies: (studies: string[]) => void; setState: (state: object) => void;
} }
>()( >()(
persist( persist(
@@ -95,10 +95,8 @@ export const useChartSettingsStore = create<
state.chartlib = lib; state.chartlib = lib;
}); });
}, },
setTradingViewStudies: (studies: string[]) => { setState: (state) => {
set((state) => { set({ state });
state.tradingViewStudies = studies;
});
}, },
})), })),
{ {
@@ -147,13 +145,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,
setTradingViewStudies: settings.setTradingViewStudies, state: settings.state,
setState: settings.setState,
}; };
}; };
@@ -13,7 +13,7 @@ import { MarketFees } from './market-fees';
import { useVolumeStats } from './use-volume-stats'; import { useVolumeStats } from './use-volume-stats';
import { useReferralStats } from './use-referral-stats'; import { useReferralStats } from './use-referral-stats';
import { formatPercentage, getAdjustedFee } from './utils'; import { formatPercentage, getAdjustedFee } from './utils';
import { Table, Td, Th, THead, Tr } from './table'; import { Table as SimpleTable } from '../../components/table';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@@ -24,6 +24,8 @@ import {
truncateMiddle, truncateMiddle,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import classNames from 'classnames';
import { getTierGradient } from '../helpers/tiers';
export const FeesContainer = () => { export const FeesContainer = () => {
const t = useT(); const t = useT();
@@ -36,7 +38,11 @@ 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({ errorPolicy: 'ignore' }); useDiscountProgramsQuery({
errorPolicy: 'ignore',
fetchPolicy: 'cache-and-network',
pollInterval: 15000,
});
const volumeDiscountWindowLength = const volumeDiscountWindowLength =
programData?.currentVolumeDiscountProgram?.windowLength || 1; programData?.currentVolumeDiscountProgram?.windowLength || 1;
@@ -47,6 +53,8 @@ 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;
@@ -166,6 +174,7 @@ export const FeesContainer = () => {
className="lg:col-span-full xl:col-span-2" className="lg:col-span-full xl:col-span-2"
loading={loading} loading={loading}
data-testid="volume-discount-card" data-testid="volume-discount-card"
noBackgroundOnMobile={true}
> >
<VolumeTiers <VolumeTiers
tiers={volumeTiers} tiers={volumeTiers}
@@ -179,12 +188,14 @@ export const FeesContainer = () => {
className="lg:col-span-full xl:col-span-2" className="lg:col-span-full xl:col-span-2"
loading={loading} loading={loading}
data-testid="referral-discount-card" data-testid="referral-discount-card"
noBackgroundOnMobile={true}
> >
<ReferralTiers <ReferralTiers
tiers={referralTiers} tiers={referralTiers}
tierIndex={referralTierIndex} tierIndex={referralTierIndex}
epochsInSet={epochsInSet} epochsInSet={epochsInSet}
referralVolumeInWindow={referralVolumeInWindow} referralVolumeInWindow={referralVolumeInWindow}
referralDiscountWindowLength={referralDiscountWindowLength}
/> />
</Card> </Card>
<Card <Card
@@ -192,6 +203,7 @@ export const FeesContainer = () => {
className="lg:col-span-full" className="lg:col-span-full"
loading={marketsLoading} loading={marketsLoading}
data-testid="fees-by-market-card" data-testid="fees-by-market-card"
noBackgroundOnMobile={true}
> >
<MarketFees <MarketFees
markets={markets} markets={markets}
@@ -477,44 +489,65 @@ const VolumeTiers = ({
return ( return (
<div> <div>
<Table> <SimpleTable
<THead> className="bg-white dark:bg-vega-cdark-900"
<Tr> columns={[
<Th data-testid="tier-header">{t('Tier')}</Th> { name: 'tier', displayName: t('Tier'), testId: 'col-tier-value' },
<Th data-testid="discount-header">{t('Discount')}</Th> {
<Th data-testid="min-volume-header">{t('Min. trading volume')}</Th> name: 'discount',
<Th data-testid="my-volume-header"> displayName: t('Discount'),
{t('myVolume', 'My volume (last {{count}} epochs)', { testId: 'discount-value',
count: windowLength, },
})} {
</Th> name: 'minTradingVolume',
<Th data-testid="actions-header" /> displayName: t('Min. trading volume'),
</Tr> testId: 'min-volume-value',
</THead> },
<tbody> {
{Array.from(tiers).map((tier, i) => { name: 'myVolume',
const isUserTier = tierIndex === i; displayName: t('myVolume', 'My volume (last {{count}} epochs)', {
count: windowLength,
return ( }),
<Tr key={i} data-testid={`tier-row-${i}`}> testId: 'my-volume-value',
<Td data-testid={`tier-value-${i}`}>{i + 1}</Td> },
<Td data-testid={`discount-value-${i}`}> {
{formatPercentage(Number(tier.volumeDiscountFactor))}% name: 'indicator',
</Td> className: 'max-md:hidden',
<Td data-testid={`min-volume-value-${i}`}> testId: 'your-tier',
{formatNumber(tier.minimumRunningNotionalTakerVolume)} },
</Td> ]}
<Td data-testid={`my-volume-value-${i}`}> data={Array.from(tiers).map((tier, i) => {
{isUserTier ? formatNumber(lastEpochVolume) : ''} const isUserTier = tierIndex === i;
</Td> const indicator = isUserTier ? (
<Td data-testid={`your-tier-${i}`}> <YourTier testId={`your-volume-tier-${i}`} />
{isUserTier ? <YourTier /> : null} ) : null;
</Td> const tierIndicator = (
</Tr> <div className="flex justify-between">
); <span data-testid={`tier-value-${i}`}>{i + 1}</span>
})} <span className="md:hidden">{indicator}</span>
</tbody> </div>
</Table> );
return {
tier: tierIndicator,
discount: (
<>{formatPercentage(Number(tier.volumeDiscountFactor))}%</>
),
minTradingVolume: (
<>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</>
),
myVolume: isUserTier ? (
formatNumber(lastEpochVolume)
) : (
<span className="md:hidden">-</span>
),
indicator: indicator,
className: classNames(
getTierGradient(i + 1, tiers.length),
'text-xs'
),
};
})}
/>
</div> </div>
); );
}; };
@@ -524,6 +557,7 @@ const ReferralTiers = ({
tierIndex, tierIndex,
epochsInSet, epochsInSet,
referralVolumeInWindow, referralVolumeInWindow,
referralDiscountWindowLength,
}: { }: {
tiers: Array<{ tiers: Array<{
referralDiscountFactor: string; referralDiscountFactor: string;
@@ -533,6 +567,7 @@ const ReferralTiers = ({
tierIndex: number; tierIndex: number;
epochsInSet: number; epochsInSet: number;
referralVolumeInWindow: number; referralVolumeInWindow: number;
referralDiscountWindowLength: number;
}) => { }) => {
const t = useT(); const t = useT();
@@ -544,60 +579,81 @@ const ReferralTiers = ({
return ( return (
<div> <div>
<Table> <SimpleTable
<THead> className="bg-white dark:bg-vega-cdark-900"
<Tr> columns={[
<Th data-testid="tier-header">{t('Tier')}</Th> { name: 'tier', displayName: t('Tier'), testId: 'col-tier-value' },
<Th data-testid="discount-header">{t('Discount')}</Th> {
<Th data-testid="min-volume-header">{t('Min. trading volume')}</Th> name: 'discount',
<Th data-testid="required-epochs-header">{t('Required epochs')}</Th> displayName: t('Discount'),
<Th data-testid="extra-header" /> tooltip: t(
</Tr> "The proportion of the referee's taker fees to be discounted"
</THead> ),
<tbody> testId: 'discount-value',
{Array.from(tiers).map((tier, i) => { },
const isUserTier = tierIndex === i; {
name: 'volume',
displayName: t(
'minTradingVolume',
'Min. trading volume (last {{count}} epochs)',
{
count: referralDiscountWindowLength,
}
),
tooltip: t(
'The minimum running notional for the given benefit tier'
),
testId: 'min-volume-value',
},
{
name: 'epochs',
displayName: t('Min. epochs'),
tooltip: t(
'The minimum number of epochs the party needs to be in the referral set to be eligible for the benefit'
),
testId: 'required-epochs-value',
},
{
name: 'indicator',
className: 'max-md:hidden',
testId: 'user-tier-or-unlocks',
},
]}
data={Array.from(tiers).map((tier, i) => {
const isUserTier = tierIndex === i;
const requiredVolume = Number(tier.minimumRunningNotionalTakerVolume);
const requiredVolume = Number( const indicator = isUserTier ? (
tier.minimumRunningNotionalTakerVolume <YourTier testId={`your-referral-tier-${i}`} />
); ) : referralVolumeInWindow >= requiredVolume &&
let unlocksIn = null; epochsInSet < tier.minimumEpochs ? (
<span className="text-muted text-xs">
Unlocks in {tier.minimumEpochs - epochsInSet} epochs
</span>
) : null;
if ( const tierIndicator = (
referralVolumeInWindow >= requiredVolume && <div className="flex justify-between">
epochsInSet < tier.minimumEpochs <span data-testid={`tier-value-${i}`}>{i + 1}</span>
) { <span className="md:hidden">{indicator}</span>
unlocksIn = ( </div>
<span className="text-muted"> );
Unlocks in {tier.minimumEpochs - epochsInSet} epochs
</span>
);
}
return ( return {
<Tr key={i} data-testid={`tier-row-${i}`}> tier: tierIndicator,
<Td data-testid={`tier-value-${i}`}>{i + 1}</Td> discount: (
<Td data-testid={`discount-value-${i}`}> <>{formatPercentage(Number(tier.referralDiscountFactor))}%</>
{formatPercentage(Number(tier.referralDiscountFactor))}% ),
</Td> volume: formatNumber(tier.minimumRunningNotionalTakerVolume),
<Td data-testid={`min-volume-value-${i}`}> epochs: tier.minimumEpochs,
{formatNumber(tier.minimumRunningNotionalTakerVolume)} indicator,
</Td> className: classNames(
<Td data-testid={`required-epochs-value-${i}`}> getTierGradient(i + 1, tiers.length),
{tier.minimumEpochs} 'text-xs'
</Td> ),
<Td data-testid={`user-tier-or-unlocks-${i}`}> };
{isUserTier ? ( })}
<YourTier testId={`your-tier-${i}`} /> />
) : (
unlocksIn
)}
</Td>
</Tr>
);
})}
</tbody>
</Table>
</div> </div>
); );
}; };
@@ -611,7 +667,7 @@ const YourTier = ({ testId }: YourTierProps) => {
return ( return (
<span <span
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white" className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white text-xs"
data-testid={testId} data-testid={testId}
> >
{t('Your tier')} {t('Your tier')}
@@ -8,43 +8,53 @@ import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { type ColDef } from 'ag-grid-community/dist/lib/entities/colDef';
const useFeesTableColumnDefs = () => { const useFeesTableColumnDefs = (): ColDef[] => {
const t = useT(); const t = useT();
return useMemo( return useMemo(
() => [ () =>
{ field: 'code', cellRenderer: 'MarketCodeCell' }, [
{ {
field: 'feeAfterDiscount', field: 'code',
headerName: t('Total fee after discount'), cellRenderer: 'MarketCodeCell',
valueFormatter: ({ value }: { value: number }) => value + '%', pinned: 'left',
}, width: 150,
{ },
field: 'infraFee', {
valueFormatter: ({ value }: { value: number }) => value + '%', field: 'liquidityFee',
}, valueFormatter: ({ value }: { value: number }) => value + '%',
{ },
field: 'makerFee', {
valueFormatter: ({ value }: { value: number }) => value + '%', field: 'feeAfterDiscount',
}, headerName: t('Total fee after discount'),
{ valueFormatter: ({ value }: { value: number }) => value + '%',
field: 'liquidityFee', },
valueFormatter: ({ value }: { value: number }) => value + '%', {
}, field: 'totalFee',
{ headerName: t('Total fee before discount'),
field: 'totalFee', valueFormatter: ({ value }: { value: number }) => value + '%',
headerName: t('Total fee before discount'), },
valueFormatter: ({ value }: { value: number }) => value + '%', {
}, field: 'infraFee',
], valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'makerFee',
valueFormatter: ({ value }: { value: number }) => value + '%',
},
] as ColDef[],
[t] [t]
); );
}; };
const feesTableDefaultColDef = { const feesTableDefaultColDef = {
flex: 1, flex: 1,
minWidth: 62,
resizable: true, resizable: true,
sortable: true, sortable: true,
suppressMovable: true,
pinned: false,
}; };
const components = { const components = {
@@ -62,6 +72,8 @@ export const MarketFees = ({
}) => { }) => {
const navigateWithMeta = useNavigateWithMeta(); const navigateWithMeta = useNavigateWithMeta();
const colDef = useFeesTableColumnDefs();
const rows = compact(markets || []).map((m) => { const rows = compact(markets || []).map((m) => {
const infraFee = new BigNumber(m.fees.factors.infrastructureFee); const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
const makerFee = new BigNumber(m.fees.factors.makerFee); const makerFee = new BigNumber(m.fees.factors.makerFee);
@@ -88,9 +100,9 @@ export const MarketFees = ({
}); });
return ( return (
<div className="border rounded-sm border-default"> <div className="border rounded-lg md:rounded-sm overflow-hidden border-default">
<AgGrid <AgGrid
columnDefs={useFeesTableColumnDefs()} columnDefs={colDef}
rowData={rows} rowData={rows}
getRowId={({ data }) => data.id} getRowId={({ data }) => data.id}
defaultColDef={feesTableDefaultColDef} defaultColDef={feesTableDefaultColDef}
@@ -1,4 +1,5 @@
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<
@@ -38,7 +38,7 @@ export const FillsContainer = () => {
); );
}; };
const useFillsStore = create<DataGridSlice>()( export const useFillsStore = create<DataGridSlice>()(
persist(createDataGridSlice, { persist(createDataGridSlice, {
name: 'vega_fills_store', name: 'vega_fills_store',
}) })
@@ -0,0 +1,8 @@
import { GridSettings } from '../grid-settings/grid-settings';
import { useFillsStore } from './fills-container';
export const FillsSettings = () => (
<GridSettings
updateGridStore={useFillsStore((store) => store.updateGridStore)}
/>
);
@@ -1 +1,2 @@
export * from './fills-container'; export * from './fills-container';
export * from './fills-settings';
@@ -45,7 +45,7 @@ export const FundingPaymentsContainer = ({
); );
}; };
const useFundingPaymentsStore = create<DataGridSlice>()( export const useFundingPaymentsStore = create<DataGridSlice>()(
persist(createDataGridSlice, { persist(createDataGridSlice, {
name: 'vega_funding_payments_store', name: 'vega_funding_payments_store',
}) })
@@ -0,0 +1,8 @@
import { GridSettings } from '../grid-settings/grid-settings';
import { useFundingPaymentsStore } from './funding-payments-container';
export const FundingPaymentsSettings = () => (
<GridSettings
updateGridStore={useFundingPaymentsStore((store) => store.updateGridStore)}
/>
);
@@ -1 +1,2 @@
export * from './funding-payments-container'; export * from './funding-payments-container';
export * from './funding-payments-settings';
@@ -0,0 +1,21 @@
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,
});
});
});
@@ -0,0 +1,24 @@
import { useT } from '../../lib/use-t';
import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
export const GridSettings = ({
updateGridStore,
}: {
updateGridStore: (gridStore: DataGridStore) => void;
}) => {
const t = useT();
return (
<Button
onClick={() =>
updateGridStore({
columnState: undefined,
filterModel: undefined,
})
}
size="extra-small"
>
{t('Reset Columns')}
</Button>
);
};

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