Compare commits

..
Author SHA1 Message Date
Matthew Russell a580f93bff chore: update specs 2023-08-17 13:25:39 +01:00
Matthew Russell b90db00710 chore: add missing field to mock position 2023-08-17 13:23:37 +01:00
Matthew Russell 98c9d34ad3 fix: reduce col width for positions 2023-08-17 13:23:37 +01:00
Matthew Russell 525a2af021 test: add test for sort and filters 2023-08-17 13:23:37 +01:00
Matthew Russell 43ae1961eb feat: add filter for closed markets 2023-08-17 13:23:37 +01:00
Matthew Russell 783f897ee7 feat: add positions menu 2023-08-17 13:23:37 +01:00
Matthew Russell 7142fc956c chore(positions): spec updates for positions table changes (#4568) 2023-08-17 13:07:14 +01:00
Matthew Russell 611f8d7491 feat(positions): table layout changes (#4558) 2023-08-17 11:07:48 +01:00
Sam Keen f3b72b894e feat(explorer): view 'Cancel Stop Order' Transactions (#4521) 2023-08-17 10:27:47 +01:00
daro-maj 140f16f637 test(trading): adjust tests edit and cancel orders (#4563) 2023-08-17 09:10:28 +01:00
Sam Keen cae1fc67a4 fix(governance): validators display fixes (#4547) 2023-08-16 22:58:14 +01:00
m.ray 35e25d9a8f chore(trading): metadata console updates (#4549) 2023-08-16 17:06:56 +01:00
Sam Keen 86e122389c fix(governance): proposal vote status header component (#4561) 2023-08-16 16:53:32 +01:00
Joe Tsang 6616aceebb test(governance): add timeout and fix withdrawal test (#4554) 2023-08-16 16:53:24 +01:00
ArtandMatthew Russell c937a9df0c feat(proposals): upgrade in progress notification (#4509)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-16 15:50:38 +01:00
MaciekandMatthew Russell 9fcf286e4b feat(trading): distinguish between product types (#4543)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-16 15:43:23 +01:00
m.ray e3ed71bec4 chore(trading): use action icon buttons (#4550) 2023-08-16 15:38:19 +01:00
daro-maj 809f149df9 test(trading): remove charts depth test (#4557) 2023-08-16 10:20:15 +02:00
94 changed files with 1716 additions and 1186 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 100
timeout-minutes: 120
steps:
# Checks if skip cache was requested
- name: Set skip-nx-cache flag
+3 -2
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_ENV=CUSTOM
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
@@ -8,3 +6,6 @@ NX_VEGA_EXPLORER_URL=/
# App flags
NX_EXPLORER_TXS_LIST=0
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
+3 -2
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_VEGA_ENV=DEVNET
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
@@ -10,3 +8,6 @@ NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_EXPLORER_URL=/
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
+3 -2
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/6242427
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_URL=https://api.vega.community/graphql
@@ -11,3 +9,6 @@ NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+4 -3
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
@@ -10,4 +8,7 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks/
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+3 -2
View File
@@ -1,7 +1,5 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
@@ -11,3 +9,6 @@ NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+3 -2
View File
@@ -7,9 +7,10 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
NX_VEGA_GOVERNANCE_URL=https://governance.validators-testnet.vega.rocks
NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks/
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
@@ -11,6 +11,7 @@ import { TxDetailsBatch } from './tx-batch';
import { TxDetailsChainEvent } from './tx-chain-event';
import { TxDetailsNodeVote } from './tx-node-vote';
import { TxDetailsOrderCancel } from './tx-order-cancel';
import { TxDetailsStopOrderCancel } from './tx-stop-order-cancel';
import { TxDetailsOrderAmend } from './tx-order-amend';
import { TxDetailsWithdrawSubmission } from './tx-withdraw-submission';
import { TxDetailsDelegate } from './tx-delegation';
@@ -85,6 +86,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsProtocolUpgrade;
case 'Cancel Order':
return TxDetailsOrderCancel;
case 'Stop Orders Cancellation':
return TxDetailsStopOrderCancel;
case 'Amend Order':
return TxDetailsOrderAmend;
case 'Validator Heartbeat':
@@ -0,0 +1,60 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import { MarketLink } from '../../links/';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { CancelSummary } from '../../order-summary/order-cancellation';
import Hash from '../../links/hash';
import type { components } from '../../../../types/explorer';
export type StopOrderCancellationTransaction =
components['schemas']['v1StopOrdersCancellation'];
interface TxDetailsStopOrderCancelProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Someone cancelled a stop order
*/
export const TxDetailsStopOrderCancel = ({
txData,
pubKey,
blockData,
}: TxDetailsStopOrderCancelProps) => {
if (!txData || !txData.command) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const command: StopOrderCancellationTransaction =
txData.command.stopOrdersCancellation;
const { marketId, stopOrderId } = command;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Cancel stop order')}</TableCell>
<TableCell>
{stopOrderId ? (
<Hash text={stopOrderId} />
) : (
<CancelSummary orderId={stopOrderId} marketId={marketId} />
)}
</TableCell>
</TableRow>
{marketId ? (
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody>
);
};
@@ -34,6 +34,7 @@ export type FilterOption =
| 'Protocol Upgrade'
| 'Register new Node'
| 'State Variable Proposal'
| 'Stop Orders Cancellation'
| 'Submit Oracle Data'
| 'Submit Order'
| 'Transfer Funds'
@@ -53,6 +54,7 @@ export const PrimaryFilterOptions: FilterOption[] = [
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Stop Orders Cancellation',
'Submit Oracle Data',
'Submit Order',
'Transfer Funds',
+9
View File
@@ -22,6 +22,11 @@ import { Footer } from '../components/footer/footer';
import { Header } from '../components/header';
import { Routes } from './route-names';
import { useExplorerNodeNamesLazyQuery } from './validators/__generated__/NodeNames';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
@@ -62,6 +67,10 @@ export const Layout = () => {
/>
)}
<Header />
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
</div>
<div className={fixedWidthClasses}>
<main className="p-4">
+1 -1
View File
@@ -17,7 +17,7 @@
<meta name="og:site_name" content="Vega Protocol - Explorer" />
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:card" content="https://static.vega.xyz/favicon.ico" />
<meta name="twitter:title" content="Vega Protocol - Explorer" />
<meta name="twitter:description" content="Vega Protocol - Explorer" />
<meta name="twitter:image" content="https://static.vega.xyz/favicon.ico" />
@@ -29,6 +29,7 @@ const toastPanel = 'toast-panel';
const toastClose = 'toast-close';
const withdrawalDialogContent = 'dialog-content';
const toastCompleteWithdrawal = 'toast-complete-withdrawal';
const scrollBar = '.ag-body-horizontal-scroll-viewport';
const usdtName = 'USDC (local)';
const usdcEthAddress = '0x1b8a1B6CBE5c93609b46D1829Cc7f3Cb8eeE23a0';
const usdcSymbol = 'tUSDC';
@@ -199,20 +200,17 @@ context(
);
cy.getByTestId(toastClose).click();
});
cy.get(tableTxHash)
.eq(1)
.should('have.text', 'Complete withdrawal')
.parent()
.within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '110.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableCreatedTimeStamp).should('not.be.empty');
});
cy.get("[row-id='0']").within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '110.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableCreatedTimeStamp).should('not.be.empty');
});
ethereumWalletConnect();
cy.get(scrollBar).scrollTo('right');
cy.getByTestId(completeWithdrawalButton).first().click();
cy.getByTestId(toast)
.last(txTimeout)
@@ -84,9 +84,10 @@ export function stakingPageAssociateTokens(
.length
) {
cy.get(tokenInputApprove, txTimeout).should('be.enabled').click();
cy.contains('Approve $VEGA Tokens for staking on Vega').should(
'be.visible'
);
cy.contains(
'Approve $VEGA Tokens for staking on Vega',
txTimeout
).should('be.visible');
cy.contains(
'Approve $VEGA Tokens for staking on Vega',
txTimeout
@@ -6,6 +6,7 @@ import { Nav } from '../nav';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
@@ -49,6 +50,7 @@ const NotificationsContainer = () => {
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : null}
@@ -626,7 +626,8 @@
"status-tendermint": "Consensus",
"status-ersatz": "Standby",
"status-pending": "Candidate",
"ersatzDescription": "To be promoted, a standby validator must have more than the lowest consensus stake, plus a bonus given to existing validators. This currently requires a minimum of {{stakeNeededForPromotion}} stake assuming no penalties. Only one validator per epoch can be promoted.",
"ersatzDescription1": "To be promoted, a standby validator must have more than the lowest consensus stake, plus a bonus given to existing validators, and only one standby validator can be promoted per epoch. Currently this requires a minimum of",
"ersatzDescription2": "stake assuming no performance penalty incurred.",
"pendingDescription1": "Anyone can",
"pendingDescriptionLinkText": "set up and run a node on Vega",
"pendingDescription2": ". A node can move from being a candidate into standby based on how much nomination it attracts, assuming it has proven reliability by sending heartbeats to the network.",
@@ -784,6 +785,7 @@
"PerformancePenaltyDescription": "Performance score is a measure of how often a validator proposed blocks in the last epoch relative to how many they should be expected to propose based on their voting power. Performance penalty is applied for having a performance score of less than 1",
"UnnormalisedVotingPowerDescription": "The voting power of the validator based on their final validator score after all penalties have been applied",
"NormalisedVotingPowerDescription": "The voting power of the validator, adjusted to ensure all validator scores sum to 1, used for distribution of rewards",
"NonConsensusVotingPowerDescription": "The voting power of the validator. Only consensus validators have voting power",
"Score": "Score",
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
+1 -1
View File
@@ -17,7 +17,7 @@
<meta name="og:site_name" content="Vega Protocol - Governance" />
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:card" content="https://static.vega.xyz/favicon.ico" />
<meta name="twitter:title" content="Vega Protocol - Governance" />
<meta name="twitter:description" content="Vega Protocol - Governance" />
<meta name="twitter:image" content="https://static.vega.xyz/favicon.ico" />
@@ -24,6 +24,7 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { FLAGS } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
@@ -36,7 +37,8 @@ jest.mock('@vegaprotocol/proposals', () => ({
const renderComponent = (
proposal: ProposalQuery['proposal'],
isListItem = true,
mocks: MockedResponse[] = []
mocks: MockedResponse[] = [],
voteState?: VoteState
) =>
render(
<AppStateProvider>
@@ -47,6 +49,7 @@ const renderComponent = (
proposal={proposal}
isListItem={isListItem}
networkParams={mockNetworkParams}
voteState={voteState}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -386,10 +389,15 @@ describe('Proposal header', () => {
closingDatetime: nextWeek.toString(),
},
});
renderComponent(proposal, true, [
// @ts-ignore generateProposal always creates an id
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_NO),
]);
renderComponent(
proposal,
true,
[
// @ts-ignore generateProposal always creates an id
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_NO),
],
VoteState.No
);
expect(await screen.findByTestId('user-voted-no')).toBeInTheDocument();
});
@@ -400,10 +408,15 @@ describe('Proposal header', () => {
closingDatetime: nextWeek.toString(),
},
});
renderComponent(proposal, true, [
// @ts-ignore generateProposal always creates an id
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_YES),
]);
renderComponent(
proposal,
true,
[
// @ts-ignore generateProposal always creates an id
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_YES),
],
VoteState.Yes
);
expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument();
});
});
@@ -8,25 +8,26 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
import { useUserVote } from '../vote-details/use-user-vote';
import { ProposalVotingStatus } from '../proposal-voting-status';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { FLAGS } from '@vegaprotocol/environment';
import Routes from '../../../routes';
import { Link } from 'react-router-dom';
import type { VoteState } from '../vote-details/use-user-vote';
export const ProposalHeader = ({
proposal,
networkParams,
isListItem = true,
voteState,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
const { t } = useTranslation();
const { voteState } = useUserVote(proposal?.id);
const change = proposal?.terms.change;
let details: ReactNode;
@@ -1,4 +1,6 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
@@ -44,11 +46,15 @@ jest.mock('../list-asset', () => ({
const renderComponent = (proposal: ProposalQuery['proposal']) => {
render(
<MemoryRouter>
<Proposal
restData={{}}
proposal={proposal as ProposalQuery['proposal']}
networkParams={mockNetworkParams}
/>
<MockedProvider>
<VegaWalletProvider>
<Proposal
restData={{}}
proposal={proposal as ProposalQuery['proposal']}
networkParams={mockNetworkParams}
/>
</VegaWalletProvider>
</MockedProvider>
</MemoryRouter>
);
};
@@ -19,6 +19,8 @@ import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import { ProposalMarketChanges } from '../proposal-market-changes';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@@ -53,6 +55,8 @@ export const Proposal = ({
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => {
const { t } = useTranslation();
const { submit, Dialog, finalizedVote } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
if (!proposal) {
return null;
@@ -132,10 +136,12 @@ export const Proposal = ({
</div>
)}
</div>
<ProposalHeader
proposal={proposal}
isListItem={false}
networkParams={networkParams}
voteState={voteState}
/>
<div id="details">
@@ -207,6 +213,10 @@ export const Proposal = ({
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
submit={submit}
dialog={Dialog}
voteState={voteState}
voteDatetime={voteDatetime}
/>
</RoundedWrapper>
</div>
@@ -1,6 +1,7 @@
import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details';
import { useUserVote } from '../vote-details/use-user-vote';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
@@ -14,12 +15,17 @@ export const ProposalsListItem = ({
proposal,
networkParams,
}: ProposalsListItemProps) => {
const { voteState } = useUserVote(proposal?.id);
if (!proposal || !proposal.id || !networkParams) return null;
return (
<li id={proposal.id} data-testid="proposals-list-item">
<RoundedWrapper paddingBottom={true} heightFull={true}>
<ProposalHeader proposal={proposal} networkParams={networkParams} />
<ProposalHeader
proposal={proposal}
networkParams={networkParams}
voteState={voteState}
/>
<ProposalsListItemDetails proposal={proposal} />
</RoundedWrapper>
</li>
@@ -188,11 +188,7 @@ export const VoteButtons = ({
(voteState === VoteState.Yes || voteState === VoteState.No) && (
<p data-testid="you-voted">
<span>{t('youVoted')}:</span>{' '}
<span
className={
voteState === VoteState.Yes ? 'text-success' : 'text-danger'
}
>
<span className="text-white font-bold">
{t(`voteState_${voteState}`)}
</span>{' '}
{voteDatetime ? (
@@ -3,23 +3,29 @@ import { formatDistanceToNow } from 'date-fns';
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import { formatNumber } from '../../../../lib/format-number';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { useVoteInformation } from '../../hooks';
import { useUserVote } from './use-user-vote';
import { CurrentProposalStatus } from '../current-proposal-status';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import { ProposalType } from '../proposal/proposal';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps } from '@vegaprotocol/wallet';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
interface VoteDetailsProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
proposalType: ProposalType | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null;
voteDatetime: Date | null;
}
export const VoteDetails = ({
@@ -27,6 +33,10 @@ export const VoteDetails = ({
minVoterBalance,
spamProtectionMinTokens,
proposalType,
submit,
dialog,
voteState,
voteDatetime,
}: VoteDetailsProps) => {
const { pubKey } = useVegaWallet();
const {
@@ -48,8 +58,7 @@ export const VoteDetails = ({
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const { submit, Dialog, finalizedVote } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
const defaultDecimals = 2;
const daysLeft = t('daysLeft', {
daysLeft: formatDistanceToNow(new Date(proposal?.terms.closingDatetime)),
@@ -219,7 +228,7 @@ export const VoteDetails = ({
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
dialog={Dialog}
dialog={dialog}
/>
)
) : (
@@ -389,10 +389,10 @@ export const ConsensusValidatorsTable = ({
},
{
field: ValidatorFields.NORMALISED_VOTING_POWER,
headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(),
headerName: t('votingPower').toString(),
headerTooltip: t('NormalisedVotingPowerDescription').toString(),
cellRenderer: VotingPowerRenderer,
width: 200,
width: 120,
},
{
field: ValidatorFields.TOTAL_PENALTIES,
@@ -20,6 +20,7 @@ import {
TotalStakeRenderer,
StakeShareRenderer,
PendingStakeRenderer,
VotingPowerRenderer,
} from './shared';
import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
@@ -39,7 +40,6 @@ interface StandbyPendingValidatorsTableProps extends ValidatorsTableProps {
export const StandbyPendingValidatorsTable = ({
data,
previousEpochData,
totalStake,
stakeNeededForPromotion,
stakeNeededForPromotionDescription,
validatorsView,
@@ -132,6 +132,8 @@ export const StandbyPendingValidatorsTable = ({
name,
},
[ValidatorFields.STAKE]: stakedTotal,
[ValidatorFields.NORMALISED_VOTING_POWER]: '0%',
[ValidatorFields.UNNORMALISED_VOTING_POWER]: '0%',
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION]:
individualStakeNeededForPromotion || null,
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION_DESCRIPTION]:
@@ -223,7 +225,14 @@ export const StandbyPendingValidatorsTable = ({
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
cellRenderer: StakeShareRenderer,
width: 100,
width: 120,
},
{
field: ValidatorFields.NORMALISED_VOTING_POWER,
headerName: t('votingPower').toString(),
headerTooltip: t('NonConsensusVotingPowerDescription').toString(),
cellRenderer: VotingPowerRenderer,
width: 120,
},
// {
// field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION,
@@ -17,6 +17,10 @@ import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import type { StakingQuery } from '../../__generated__/Staking';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorWithUserData } from './shared';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
export interface ValidatorsTableProps {
nodesData: NodesQuery | undefined;
@@ -41,6 +45,9 @@ export const ValidatorTables = ({
const {
appState: { decimals },
} = useAppState();
const { params } = useNetworkParams([
NetworkParams.network_validators_incumbentBonus,
]);
const [validatorsView, setValidatorsView] = useState<ValidatorsView>('all');
const totalStake = useMemo(
@@ -52,6 +59,10 @@ export const ValidatorTables = ({
() => userStakingData?.party?.stakingSummary.currentStakeAvailable || '0',
[userStakingData?.party?.stakingSummary.currentStakeAvailable]
);
const incumbentBonus = useMemo(
() => new BigNumber(params?.network_validators_incumbentBonus),
[params?.network_validators_incumbentBonus]
);
let stakeNeededForPromotion = undefined;
let delegations: StakingDelegationFieldsFragment[] | undefined = undefined;
@@ -126,28 +137,23 @@ export const ValidatorTables = ({
consensusValidators.length &&
(standbyValidators.length || pendingValidators.length)
) {
const lowestRankingConsensusScore = consensusValidators.reduce(
const lowestConsensusStake = consensusValidators.reduce(
(lowest: ValidatorWithUserData, validator: ValidatorWithUserData) => {
if (
Number(validator.rankingScore.rankingScore) <
Number(lowest.rankingScore.rankingScore)
) {
if (Number(validator.stakedTotal) < Number(lowest.stakedTotal)) {
lowest = validator;
}
return lowest;
}
).rankingScore.rankingScore;
).stakedTotal;
const lowestRankingBigNum = toBigNum(lowestRankingConsensusScore, 0);
const consensusStakedTotal = consensusValidators.reduce((acc, cur) => {
return acc.plus(toBigNum(cur.stakedTotal, decimals));
}, new BigNumber(0));
const lowestRankingBigNum = toBigNum(lowestConsensusStake, decimals);
stakeNeededForPromotion = formatNumber(
lowestRankingBigNum.times(consensusStakedTotal),
lowestRankingBigNum.multipliedBy(incumbentBonus.plus(1)),
2
).toString();
}
return (
<section data-testid="validator-tables">
<div className="grid w-full justify-end">
@@ -187,12 +193,18 @@ export const ValidatorTables = ({
<div className="mb-10">
<SubHeading title={t('status-ersatz')} />
<p>
<Trans
i18nKey="ersatzDescription"
values={{
stakeNeededForPromotion,
}}
/>
<span>
<Trans i18nKey="ersatzDescription1" />
</span>
<span className="text-white font-bold">
{' '}
{stakeNeededForPromotion}{' '}
</span>
<span>
<Trans i18nKey="ersatzDescription2" />
</span>
</p>
<StandbyPendingValidatorsTable
data={standbyValidators}
+1 -1
View File
@@ -68,7 +68,7 @@ export const calculateOverstakedPenalty = (nodeId: string, nodes: Node[]) => {
}
const penalty = new BigNumber(1)
.minus(
new BigNumber(node.rewardScore?.rawValidatorScore || 0).dividedBy(tts)
new BigNumber(node.rewardScore?.rawValidatorScore || 1).dividedBy(tts)
)
.times(100);
return penalty.isLessThan(0) ? new BigNumber(0) : penalty;
@@ -256,6 +256,7 @@ describe('Closed markets', { tags: '@smoke' }, () => {
cy.get(rowSelector)
.first()
.find('[col-id="code"]')
.find('[data-testid="market-code"]')
.should('have.text', settledMarket.tradableInstrument.instrument.code);
// 6001-MARK-002
@@ -4,7 +4,8 @@ import * as Schema from '@vegaprotocol/types';
const rowSelector =
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
const colInstrumentCode =
'[col-id="tradableInstrument.instrument.code"] [data-testid="market-code"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -40,21 +40,25 @@ describe('markets selector', { tags: '@smoke' }, () => {
code: 'SOLUSD',
markPrice: '84.41',
vol: '0.00',
productType: 'Futr',
},
{
code: 'ETHBTC.QM21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'BTCUSD.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'AAPL.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
];
cy.getByTestId('header-title').should('be.visible').click();
@@ -64,7 +68,9 @@ describe('markets selector', { tags: '@smoke' }, () => {
const market = data[i];
// 6001-MARK-021
// 6001-MARK-022
expect(item.find('h3').text()).equals(market.code);
expect(item.find('h3').text()).equals(
`${market.code} ${market.productType}`
);
expect(
item.find('[data-testid="market-selector-volume"]').text()
).contains(market.vol);
@@ -3,7 +3,7 @@ import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"]';
const colMarketId = '[col-id="market"] [data-testid="market-code"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => {
@@ -155,7 +155,13 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
'AAVEDAI.MF21',
'AAPL.MF21',
];
checkSorting('market', marketColDefault, marketColAsc, marketColDesc);
checkSorting(
'market',
marketColDefault,
marketColAsc,
marketColDesc,
' [data-testid="market-code"]'
);
const stateColDefault = [
'Open',
@@ -120,7 +120,9 @@ describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('All').click();
cy.getByTestId('tab-orders')
.get(`.ag-center-cols-container [col-id='${orderSymbol}']`)
.get(
`.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]`
)
.should('have.length.at.least', expectedOrderList.length)
.then(($symbols) => {
const symbolNames: string[] = [];
@@ -445,8 +447,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="edit"]')
.should('have.text', 'Edit')
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click();
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
@@ -474,8 +475,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('have.text', 'Cancel')
.find(`[data-testid="icon-cross"]`)
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {
@@ -512,8 +512,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="edit"]')
.should('have.text', 'Edit')
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
@@ -1,371 +0,0 @@
import { checkSorting, aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock';
// #region consts
const closePosition = 'close-position';
const dialogCloseX = 'dialog-close';
const dialogContent = 'dialog-content';
const dropDownMenu = 'dropdown-menu';
const marketActionsContent = 'position-actions-content';
const positions = 'Positions';
const tabPositions = 'tab-positions';
const toastContent = 'toast-content';
const tooltipContent = 'tooltip-content';
// #endregion
describe('positions', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
});
it('renders positions on trading page', () => {
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed();
});
// TODO: move this to sim, its flakey
it.skip('renders positions on portfolio page', () => {
cy.mockGQL((req) => {
const positions = positionsQuery();
if (positions.positions?.edges) {
positions.positions.edges.push(
...positions.positions.edges.map((edge) => ({
...edge,
node: {
...edge.node,
party: {
...edge.node.party,
id: 'vega-1',
},
},
}))
);
}
aliasGQLQuery(req, 'Positions', positions);
});
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed(true);
});
it('Close my position', () => {
visitAndClickPositions();
cy.getByTestId(closePosition).first().click();
// 7004-POSI-010
cy.getByTestId(toastContent).should(
'contain.text',
'Awaiting confirmation'
);
});
});
describe('positions', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
});
it('rows should be displayed despite errors', () => {
const errors = [
{
message:
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['marketsConnection', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const marketData = marketsDataQuery();
const edges = marketData.marketsConnection?.edges.map((market) => {
const replace =
market.node.data?.market.id === 'market-2' ? null : market.node.data;
return { ...market, node: { ...market.node, data: replace } };
});
const overrides = {
...marketData,
marketsConnection: { ...marketData.marketsConnection, edges },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
const emptyCells = [
'notional',
'markPrice',
'currentLeverage',
'averageEntryPrice',
];
cy.getByTestId(tabPositions)
.first()
.within(() => {
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(0)
.within(() => {
emptyCells.forEach((cell) => {
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
});
});
});
});
it('error message should be displayed', () => {
const errors = [
{
message:
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['assets', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const overrides = {
marketsConnection: { edges: [] },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
cy.getByTestId(tabPositions).contains('no market data');
});
it('sorting by Market', () => {
visitAndClickPositions();
const marketsSortedDefault = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
const marketsSortedAsc = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
const marketsSortedDesc = [
'SOLUSD',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAPL.MF21',
];
cy.getByTestId(positions).click();
// 7004-POSI-003
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
// let elementWidth: number;
it('Resize column', () => {
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(250, 0)
.realMouseUp();
});
// 7004-POSI-006
cy.get(`[col-id="marketName"]`)
.invoke('width')
.should('be.greaterThan', 250);
});
// This test depends on the previous one
it('Has persisted column widths', () => {
const width = 400;
cy.window().then((win) => {
win.localStorage.setItem(
'vega_positions_store',
JSON.stringify({
state: {
gridStore: {
columnState: [{ colId: 'marketName', width }],
},
},
})
);
});
visitAndClickPositions();
// 7004-POSI-012
cy.get('.ag-center-cols-container .ag-row')
.first()
.find('[col-id="marketName"]')
.invoke('outerWidth')
.should('equal', width);
});
it('Scroll horizontally', () => {
visitAndClickPositions();
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(400, 0)
.realMouseUp();
});
cy.get('[col-id="marketName"]').should('be.visible');
cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({
deltaX: 500,
});
// 7004-POSI-004
cy.get('[col-id="unrealisedPNL"]').should('be.visible');
});
it('Drag and drop columns', () => {
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('[col-id="marketName"]')
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
// 7004-POSI-005
cy.get('[col-id="marketName"]').should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
it('I can see warnings', () => {
visitAndClickPositions();
cy.get('[col-id="openVolume"]')
.eq(3)
.within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
// 7004-POSI-011
cy.getByTestId(tooltipContent).should('be.visible');
});
it('Positive and Negative color change', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
// 7004-POSI-007
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="realisedPNL"]',
'text-market-green-600',
'text-market-red'
);
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="unrealisedPNL"]',
'text-market-green-600',
'text-market-red'
);
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="openVolume"]',
'text-market-green-600',
'text-market-red'
);
});
});
it('View settlement asset', () => {
visitAndClickPositions();
cy.get('[col-id="asset"]')
.eq(3)
.within(() => {
cy.get('button[type="button"]').click();
});
// 7004-POSI-008
cy.getByTestId(dialogContent).should('be.visible');
cy.getByTestId(dialogCloseX).click();
cy.getByTestId(dropDownMenu).first().click();
cy.getByTestId(marketActionsContent).click();
// 7004-POSI-009
cy.getByTestId(dialogContent).should('be.visible');
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions')
.get('.ag-center-cols-container .ag-row')
.eq(multiKey ? 3 : 1)
.within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
.invoke('text')
.should('not.be.empty');
cy.get('[col-id="openVolume"]').should('not.be.empty');
// includes average entry price, mark price, realised PNL & leverage
cy.getByTestId('flash-cell').should('not.be.empty');
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2,767.3');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').should('not.be.empty');
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
});
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
function assertPNLColor(
pnlSelector: string,
positiveClass: string,
negativeClass: string
) {
cy.get(pnlSelector).each(($el) => {
const value = parseFloat($el.text());
if (value > 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass);
} else if (value < 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass);
} else if (value == 0) {
cy.wrap($el)
.invoke('attr', 'class')
.should('not.contain', negativeClass, positiveClass);
} else {
throw new Error('Unexpected value');
}
});
}
function visitAndClickPositions() {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
}
+3
View File
@@ -20,3 +20,6 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+4 -1
View File
@@ -20,4 +20,7 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
+4 -1
View File
@@ -18,4 +18,7 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
+3
View File
@@ -21,3 +21,6 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+4 -1
View File
@@ -20,4 +20,7 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+4 -1
View File
@@ -19,4 +19,7 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+4 -1
View File
@@ -19,4 +19,7 @@ NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
@@ -94,7 +94,11 @@ const MainGrid = memo(
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom">
<Tab id="positions" name={t('Positions')}>
<Tab
id="positions"
name={t('Positions')}
menu={<TradingViews.positions.menu />}
>
<TradingViews.positions.component />
</Tab>
<Tab
@@ -17,6 +17,7 @@ import type { OrderContainerProps } from '../../components/orders-container';
import { OrdersContainer } from '../../components/orders-container';
import { StopOrdersContainer } from '../../components/stop-orders-container';
import { AccountsMenu } from '../../components/accounts-menu';
import { PositionsMenu } from '../../components/positions-menu';
type MarketDependantView =
| typeof CandlesChartContainer
@@ -57,7 +58,11 @@ export const TradingViews = {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
positions: {
label: 'Positions',
component: PositionsContainer,
menu: PositionsMenu,
},
activeOrders: {
label: 'Active',
component: (props: OrderContainerProps) => (
@@ -214,7 +214,6 @@ describe('Closed', () => {
</MemoryRouter>
);
});
// screen.debug(document, Infinity);
const headers = screen.getAllByRole('columnheader');
const expectedHeaders = [
@@ -434,7 +433,7 @@ describe('Closed', () => {
await waitFor(() => {
expect(
screen.getByRole('button', { name: 'SuccessorCode' })
screen.getByRole('button', { name: /^SuccessorCode/ })
).toBeInTheDocument();
});
expect(
@@ -443,6 +442,16 @@ describe('Closed', () => {
element.getAttribute('col-id') === 'successorMarket',
})
).toBeInTheDocument();
screen
.getAllByRole('gridcell', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarket',
})
.forEach((element) => {
expect(element.querySelector('[title="Future"]')?.textContent).toEqual(
'Futr'
);
});
});
it('feature flag should hide successors', async () => {
+9 -12
View File
@@ -4,7 +4,11 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
COL_DEFS,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
@@ -47,6 +51,7 @@ interface Row {
setlementDataSourceFilter: DataSourceFilterFragment | undefined;
tradingTerminationOracleId: string;
settlementAsset: SettlementAsset;
productType: string;
}
export const Closed = () => {
@@ -90,6 +95,7 @@ export const Closed = () => {
tradingTerminationOracleId:
instrument.product.dataSourceSpecForTradingTermination.id,
settlementAsset: instrument.product.settlementAsset,
productType: instrument.product.__typename || '',
};
return row;
@@ -115,16 +121,7 @@ const ClosedMarketsDataGrid = ({
{
headerName: t('Market'),
field: 'code',
cellRenderer: ({
value,
data,
}: VegaICellRendererParams<Row, 'code'>) => {
return (
<span data-testid="market-code" data-market-id={data?.id}>
{value}
</span>
);
},
cellRenderer: 'MarketNameCell',
},
{
headerName: t('Description'),
@@ -279,7 +276,7 @@ const ClosedMarketsDataGrid = ({
rowData={rowData}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
components={{ SuccessorMarketRenderer }}
components={{ SuccessorMarketRenderer, MarketNameCell }}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
/>
);
@@ -0,0 +1,190 @@
import { render, screen, act, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import userEvent from '@testing-library/user-event';
import type { Market } from '@vegaprotocol/markets';
import { SuccessorMarketRenderer } from './successor-market-cell';
import {
MarketsDocument,
SuccessorMarketIdsDocument,
} from '@vegaprotocol/markets';
import { createMarketFragment } from '@vegaprotocol/mock';
const mockSuccessorsQuery = [
{
id: 'market1',
parentMarketID: 'parentMarket1',
successorMarketID: 'successorMarket1',
},
{ id: 'market2', parentMarketID: 'parentMarket2' },
{ id: 'market3', successorMarketID: 'successorMarket3' },
];
const parentMarket1 = {
id: 'parentMarket1',
tradableInstrument: {
instrument: { code: 'code parent 1', id: '1' },
},
} as unknown as Market;
const successorMarket1 = {
id: 'successorMarket1',
tradableInstrument: {
instrument: { code: 'code successor 1', id: '2' },
},
} as unknown as Market;
const parentMarket2 = {
id: 'parentMarket2',
tradableInstrument: {
instrument: { code: 'code parent 2', id: '3' },
},
} as unknown as Market;
const successorMarket3 = {
id: 'successorMarket3',
tradableInstrument: {
instrument: { code: 'code successor 3', id: '4' },
},
} as unknown as Market;
const mockMarkets = [
parentMarket1,
successorMarket1,
parentMarket2,
successorMarket3,
];
const mockClickHandler = jest.fn();
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
useMarketClickHandler: jest.fn().mockImplementation(() => mockClickHandler),
}));
const marketMock = {
request: {
query: MarketsDocument,
variables: undefined,
},
result: {
data: {
marketsConnection: {
edges: mockMarkets.map((item) => ({
node: {
...createMarketFragment(item),
},
})),
},
},
},
};
const successorMock = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
edges: mockSuccessorsQuery.map((item) => ({
node: {
...item,
},
})),
},
},
},
};
const mocks = [marketMock, successorMock];
describe('SuccessorMarketRenderer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should properly rendered successor market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket1', false);
});
});
it('should properly rendered parent market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket1', false);
});
});
it('should properly rendered only parent market', async () => {
const successorValue = 'market2';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 2')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket2', false);
});
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should properly rendered only successor market', async () => {
const successorValue = 'market3';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 3')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket3', false);
});
await act(() => {
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
});
expect(screen.getByText('-')).toBeInTheDocument();
});
});
@@ -1,8 +1,8 @@
import React from 'react';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketProvider, useSuccessorMarketIds } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import React from 'react';
export const SuccessorMarketRenderer = ({
value,
@@ -33,6 +33,7 @@ export const SuccessorMarketRenderer = ({
value={data.tradableInstrument.instrument.code}
data={data}
onMarketClick={onMarketClick}
productType={data.tradableInstrument.instrument?.product.__typename}
/>
) : (
'-'
@@ -99,6 +99,7 @@ describe('MarketSelectorItem', () => {
currentMarketId={market.id}
style={{}}
onSelect={jest.fn()}
allProducts
/>
</MockedProvider>
</MemoryRouter>
@@ -181,5 +182,7 @@ describe('MarketSelectorItem', () => {
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
);
});
expect(screen.getByText('Futr')).toBeInTheDocument();
});
});
@@ -12,17 +12,20 @@ import {
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { MarketProductPill } from '@vegaprotocol/datagrid';
export const MarketSelectorItem = ({
market,
style,
currentMarketId,
onSelect,
allProducts,
}: {
market: MarketMaybeWithDataAndCandles;
style: CSSProperties;
currentMarketId?: string;
onSelect: (marketId: string) => void;
allProducts: boolean;
}) => {
return (
<div style={style} role="row">
@@ -36,13 +39,19 @@ export const MarketSelectorItem = ({
})}
onClick={() => onSelect(market.id)}
>
<MarketData market={market} />
<MarketData market={market} allProducts={allProducts} />
</Link>
</div>
);
};
const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
const MarketData = ({
market,
allProducts,
}: {
market: MarketMaybeWithDataAndCandles;
allProducts: boolean;
}) => {
const { data } = useMarketDataUpdateSubscription({
variables: {
marketId: market.id,
@@ -80,11 +89,16 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
? addDecimalsFormatNumber(vol, market.positionDecimalPlaces)
: '0.00';
const productType = market.tradableInstrument.instrument.product.__typename;
return (
<>
<div className="w-2/5" role="gridcell">
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
{market.tradableInstrument.instrument.code}
{market.tradableInstrument.instrument.code}{' '}
{allProducts && productType && (
<MarketProductPill productType={productType} />
)}
</h3>
{mode && (
<p className="text-xs text-vega-orange-500 dark:text-vega-orange-550 whitespace-nowrap">
@@ -43,7 +43,7 @@ export const MarketSelector = ({
sort: Sort.None,
assets: [],
});
const allProducts = filter.product === Product.All;
const { markets, data, loading, error } = useMarketSelectorList(filter);
return (
@@ -128,6 +128,7 @@ export const MarketSelector = ({
? t('Spot markets coming soon.')
: t('No markets')
}
allProducts={allProducts}
/>
</div>
</div>
@@ -141,6 +142,7 @@ const MarketList = ({
currentMarketId,
onSelect,
noItems,
allProducts,
}: {
data: MarketMaybeWithDataAndCandles[];
error: Error | undefined;
@@ -149,6 +151,7 @@ const MarketList = ({
currentMarketId?: string;
onSelect: (marketId: string) => void;
noItems: string;
allProducts: boolean;
}) => {
const itemSize = 45;
const listRef = useRef<HTMLDivElement | null>(null);
@@ -192,6 +195,7 @@ const MarketList = ({
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={noItems}
allProducts={allProducts}
/>
</div>
</TinyScroll>
@@ -202,6 +206,7 @@ interface ListItemData {
data: MarketMaybeWithDataAndCandles[];
onSelect: (marketId: string) => void;
currentMarketId?: string;
allProducts: boolean;
}
const ListItem = ({
@@ -218,6 +223,7 @@ const ListItem = ({
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
allProducts={data.allProducts}
/>
);
@@ -229,19 +235,21 @@ const List = ({
onSelect,
noItems,
currentMarketId,
allProducts,
}: ListItemData & {
loading: boolean;
height: number;
itemSize: number;
noItems: string;
allProducts: boolean;
}) => {
const itemKey = useCallback(
(index: number, data: ListItemData) => data.data[index].id,
[]
);
const itemData = useMemo(
() => ({ data, onSelect, currentMarketId }),
[data, onSelect, currentMarketId]
() => ({ data, onSelect, currentMarketId, allProducts }),
[data, onSelect, currentMarketId, allProducts]
);
if (!data || loading) {
return (
@@ -5,6 +5,7 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import type { StateCreator } from 'zustand';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
@@ -13,6 +14,7 @@ export const PositionsContainer = ({ allKeys }: { allKeys?: boolean }) => {
const onMarketClick = useMarketClickHandler(true);
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
const showClosed = usePositionsStore((store) => store.showClosedMarkets);
const gridStore = usePositionsStore((store) => store.gridStore);
const updateGridStore = usePositionsStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, updateGridStore);
@@ -40,12 +42,35 @@ export const PositionsContainer = ({ allKeys }: { allKeys?: boolean }) => {
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
gridProps={gridStoreCallbacks}
showClosed={showClosed}
/>
);
};
const usePositionsStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_positions_store',
})
type PositionsStoreSlice = {
showClosedMarkets: boolean;
toggleClosedMarkets: () => void;
};
const createPositionStoreSlice: StateCreator<PositionsStoreSlice> = (set) => ({
showClosedMarkets: false,
toggleClosedMarkets: () => {
set((curr) => {
return {
showClosedMarkets: !curr.showClosedMarkets,
};
});
},
});
export const usePositionsStore = create<PositionsStoreSlice & DataGridSlice>()(
persist(
(...args) => ({
...createPositionStoreSlice(...args),
...createDataGridSlice(...args),
}),
{
name: 'vega_positions_store',
}
)
);
@@ -0,0 +1 @@
export * from './positions-menu';
@@ -0,0 +1,18 @@
import { t } from '@vegaprotocol/i18n';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { usePositionsStore } from '../positions-container';
export const PositionsMenu = () => {
const showClosed = usePositionsStore((store) => store.showClosedMarkets);
const toggle = usePositionsStore((store) => store.toggleClosedMarkets);
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
data-testid="open-transfer"
onClick={toggle}
>
{showClosed ? t('Hide closed markets') : t('Show closed markets')}
</TradingButton>
);
};
+2
View File
@@ -43,6 +43,7 @@ import { Navbar } from '../components/navbar';
import classNames from 'classnames';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner';
@@ -111,6 +112,7 @@ function AppBody({ Component }: AppProps) {
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
<ViewingBanner />
<UpgradeBanner showVersionChange={true} />
</div>
+34 -1
View File
@@ -5,12 +5,45 @@ export default function Document() {
<Html>
<Head>
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charSet="utf-8" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Vega Protocol - VEGA Console" />
<meta name="og:type" content="website" />
<meta name="og:url" content="https://console.vega.xyz/" />
<meta name="og:title" content="Vega Protocol - Console" />
<meta name="og:site_name" content="Vega Protocol - Console" />
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
<meta
name="twitter:card"
content="https://static.vega.xyz/favicon.ico"
/>
<meta name="twitter:title" content="Vega Protocol - Console" />
<meta name="twitter:description" content="Vega Protocol - Console" />
<meta
name="twitter:image"
content="https://static.vega.xyz/favicon.ico"
/>
<meta name="twitter:image:alt" content="VEGA logo" />
<meta name="twitter:site" content="@vegaprotocol" />
<meta name="description" content="Vega Protocol - Console" />
<link
rel="apple-touch-icon"
content="https://static.vega.xyz/favicon.ico"
/>
<link
rel="preload"
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/preloader.css" media="all" />
+1 -27
View File
@@ -1,4 +1,3 @@
import Head from 'next/head';
import { ClientRouter } from './client-router';
/**
@@ -7,30 +6,5 @@ import { ClientRouter } from './client-router';
* have to serve a static site via next export
*/
export default function Index() {
return (
<>
<Head>
<meta charSet="utf-8" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Vega Protocol - Console" />
<link rel="apple-touch-icon" href="assets/apple-touch-icon.png" />
<link rel="manifest" href="assets/manifest.json" />
<link
rel="preload"
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
as="font"
type="font/woff2"
/>
<title>VEGA Console dApp</title>
</Head>
<ClientRouter />
</>
);
return <ClientRouter />;
}
@@ -24,6 +24,9 @@ const singleRow = {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
product: {
__typename: 'Future',
},
},
},
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
@@ -120,6 +123,9 @@ describe('BreakdownTable', () => {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
product: {
__typename: 'Future',
},
},
},
},
+15 -5
View File
@@ -36,14 +36,24 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
valueFormatter: ({
minWidth: 200,
cellRenderer: ({
value,
}: VegaValueFormatterParams<
data,
}: VegaICellRendererParams<
AccountFields,
'market.tradableInstrument.instrument.code'
>) => {
if (!value) return 'None';
return value;
return value ? (
<MarketNameCell
value={value}
productType={
data?.market?.tradableInstrument.instrument.product.__typename
}
/>
) : (
'None'
);
},
},
{
@@ -126,7 +136,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
}
ref={ref}
rowHeight={34}
components={{ PriceCell, MarketNameCell, ProgressBarCell }}
components={{ PriceCell, ProgressBarCell }}
tooltipShowDelay={500}
defaultColDef={defaultColDef}
columnDefs={coldefs}
+6 -3
View File
@@ -121,9 +121,12 @@ export function createClient({
// called on startup, NodeCheck and NodeCheckTimeUpdate are called
// by the NodeSwitcher component and the useNodeHealth hook
if (
['NodeGuard', 'NodeCheck', 'NodeCheckTimeUpdate'].includes(
operation.operationName
)
[
'NodeGuard',
'NodeCheck',
'NodeCheckTimeUpdate',
'BlockStatistics',
].includes(operation.operationName)
) {
return;
}
+14 -6
View File
@@ -41,21 +41,22 @@ export const checkSorting = (
column: string,
orderTabDefault: string[],
orderTabAsc: string[],
orderTabDesc: string[]
orderTabDesc: string[],
additionalColumnSelector = ''
) => {
checkSortChange(orderTabDefault, column);
checkSortChange(orderTabDefault, column, additionalColumnSelector);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).last().click();
});
checkSortChange(orderTabAsc, column);
checkSortChange(orderTabAsc, column, additionalColumnSelector);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabDesc, column);
checkSortChange(orderTabDesc, column, additionalColumnSelector);
cy.get('.ag-header-container')
.last()
.within(() => {
@@ -63,13 +64,20 @@ export const checkSorting = (
});
};
const checkSortChange = (tabsArr: string[], column: string) => {
const checkSortChange = (
tabsArr: string[],
column: string,
additionalColumnSelector = ''
) => {
cy.get('.ag-center-cols-container')
.last()
.within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
cy.get(`[col-id="${column}"]${additionalColumnSelector}`).should(
'have.text',
tabsArr[i]
);
});
});
});
@@ -9,7 +9,10 @@ export const CenteredGridCellWrapper = ({
className?: string;
}) => (
<div
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
className={classNames(
'flex h-[20px] p-0 justify-items-center items-center',
className
)}
>
<div className="w-full self-center">{children}</div>
</div>
@@ -1,13 +1,38 @@
import type { MouseEvent, ReactNode } from 'react';
import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import get from 'lodash/get';
import { Pill } from '@vegaprotocol/ui-toolkit';
import {
ProductTypeShortName,
type Market,
type ProductType,
ProductTypeMapping,
} from '@vegaprotocol/types';
export const MarketProductPill = ({
productType,
}: {
productType: ProductType;
}) => {
return (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProductTypeMapping[productType]}
>
{ProductTypeShortName[productType]}
</Pill>
);
};
interface MarketNameCellProps {
value?: string | null;
data?: { id?: string; marketId?: string; market?: { id: string } };
data?:
| { id?: string; marketId?: string; productType?: string; market?: Market }
| Market;
idPath?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
defaultValue?: ReactNode;
productType?: ProductType;
}
export const MarketNameCell = ({
@@ -15,29 +40,43 @@ export const MarketNameCell = ({
data,
idPath,
onMarketClick,
productType,
}: MarketNameCellProps) => {
const id = data ? get(data, idPath ?? 'id', 'all') : '';
const handleOnClick = useCallback(
(ev: MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
if (onMarketClick) {
onMarketClick(id, ev.metaKey || ev.ctrlKey);
}
id && onMarketClick?.(id, ev.metaKey || ev.ctrlKey);
},
[id, onMarketClick]
);
if (!value || !data) return null;
return onMarketClick ? (
productType =
productType ||
(data as { productType?: ProductType }).productType ||
(data as Market)?.tradableInstrument?.instrument.product.__typename ||
(data as { market: Market })?.market?.tradableInstrument.instrument.product
.__typename;
if (!value) return;
const content = (
<>
<span data-testid="market-code" data-market-id={id}>
{value}
</span>
{productType && <MarketProductPill productType={productType} />}
</>
);
return onMarketClick && id ? (
<button
onClick={handleOnClick}
tabIndex={0}
className="block text-left text-ellipsis overflow-hidden whitespace-nowrap w-full"
>
{value}
{content}
</button>
) : (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>{value}</>
content
);
};
+5 -3
View File
@@ -29,6 +29,7 @@ const EmptyLinks: DAppLinks = {
const ExplorerLinks = {
...EmptyLinks,
[Networks.STAGNET1]: 'https://explorer.stagnet1.vega.rocks',
[Networks.TESTNET]: 'https://explorer.fairground.wtf',
[Networks.VALIDATOR_TESTNET]:
'https://explorer.validators-testnet.vega.rocks',
@@ -40,17 +41,18 @@ const ConsoleLinks = {
...EmptyLinks,
[Networks.STAGNET1]: 'https://trading.stagnet1.vega.rocks',
[Networks.TESTNET]: 'https://console.fairground.wtf',
[Networks.MAINNET]: 'https://console.vega.xyz',
[Networks.MAINNET_MIRROR]: 'https://console.mainnet-mirror.vega.rocks/',
[Networks.MAINNET]: 'https://vega.trading',
[Networks.MAINNET_MIRROR]: 'https://console.mainnet-mirror.vega.rocks',
};
const TokenLinks = {
...EmptyLinks,
[Networks.DEVNET]: 'https://dev.governance.vega.xyz',
[Networks.STAGNET1]: 'https://governance.stagnet1.vega.rocks',
[Networks.TESTNET]: 'https://governance.fairground.wtf',
[Networks.VALIDATOR_TESTNET]:
'https://governance.validators-testnet.vega.rocks',
[Networks.MAINNET_MIRROR]: 'https://governance.mainnet-mirror.vega.rocks/',
[Networks.MAINNET_MIRROR]: 'https://governance.mainnet-mirror.vega.rocks',
[Networks.MAINNET]: 'https://governance.vega.xyz',
};
@@ -60,7 +60,7 @@ export const ledgerEntriesProvider = makeDerivedDataProvider<
(partsData) => {
const entries = partsData[0] as ReturnType<typeof getData>;
const assets = partsData[1] as Record<string, Asset>;
const markets = partsData[1] as Record<string, Market>;
const markets = partsData[2] as Record<string, Market>;
return entries.map((entry) => {
const asset = entry.assetId
? (assets as Record<string, Asset>)[entry.assetId]
+15 -2
View File
@@ -12,10 +12,12 @@ import type {
import {
AgGridLazy as AgGrid,
DateRangeFilter,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import type * as Types from '@vegaprotocol/types';
import type { ColDef } from 'ag-grid-community';
import type { Market } from '@vegaprotocol/markets';
import {
AccountTypeMapping,
DescriptionTransferTypeMapping,
@@ -82,10 +84,14 @@ export const LedgerTable = (props: LedgerEntryProps) => {
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-',
>) =>
value && (
<MarketNameCell value={value} data={data?.marketSender as Market} />
),
},
{
headerName: t('Receiver'),
@@ -112,10 +118,17 @@ export const LedgerTable = (props: LedgerEntryProps) => {
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-',
>) =>
value && (
<MarketNameCell
value={value}
data={data?.marketReceiver as Market}
/>
),
},
{
headerName: t('Transfer type'),
@@ -107,6 +107,7 @@ export const NetworkParams = {
market_liquidity_targetstake_triggering_ratio:
'market_liquidity_targetstake_triggering_ratio',
transfer_fee_factor: 'transfer_fee_factor',
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
} as const;
type Params = typeof NetworkParams;
@@ -80,7 +80,7 @@ describe('OrderListTable', () => {
'-',
Schema.OrderTimeInForceCode[marketOrder.timeInForce],
getDateTimeFormat().format(new Date(marketOrder.createdAt)),
'Edit',
'',
];
expectedValues.forEach((expectedValue, i) =>
expect(cells[i]).toHaveTextContent(expectedValue)
@@ -104,7 +104,7 @@ describe('OrderListTable', () => {
Schema.OrderTimeInForceCode[limitOrder.timeInForce]
}: ${getDateTimeFormat().format(new Date(limitOrder.expiresAt ?? ''))}`,
getDateTimeFormat().format(new Date(limitOrder.createdAt)),
'Edit',
'',
];
expectedValues.forEach((expectedValue, i) =>
expect(cells[i]).toHaveTextContent(expectedValue)
@@ -271,8 +271,8 @@ export const OrderListTable = memo<
{
colId: 'amend',
...COL_DEFS.actions,
minWidth: showAllActions ? 110 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 110 : COL_DEFS.actions.minWidth,
minWidth: showAllActions ? 90 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 90 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: Order }) => {
if (!data) return null;
@@ -285,14 +285,14 @@ export const OrderListTable = memo<
data-testid="edit"
onClick={() => onEdit(data)}
>
{t('Edit')}
<VegaIcon name={VegaIconNames.EDIT} size={16} />
</ButtonLink>
)}
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
>
{t('Cancel')}
<VegaIcon name={VegaIconNames.CROSS} size={16} />
</ButtonLink>
</>
)}
+46 -37
View File
@@ -1,18 +1,18 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useEstimatePositionQuery } from './__generated__/Positions';
import { formatRange } from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
export const LiquidationPrice = ({
marketId,
openVolume,
collateralAvailable,
decimalPlaces,
formatDecimals,
marketDecimalPlaces,
}: {
marketId: string;
openVolume: string;
collateralAvailable: string;
decimalPlaces: number;
formatDecimals: number;
marketDecimalPlaces: number;
}) => {
const { data: currentData, previousData } = useEstimatePositionQuery({
variables: {
@@ -23,38 +23,47 @@ export const LiquidationPrice = ({
fetchPolicy: 'no-cache',
skip: !openVolume || openVolume === '0',
});
const data = currentData || previousData;
let value = '-';
if (data) {
const bestCase =
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
/\..*/,
''
);
const worstCase =
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
/\..*/,
''
);
value =
bestCase && worstCase && BigInt(bestCase) < BigInt(worstCase)
? formatRange(
bestCase,
worstCase,
decimalPlaces,
undefined,
formatDecimals,
value
)
: formatRange(
worstCase,
bestCase,
decimalPlaces,
undefined,
formatDecimals,
value
);
const data = currentData || previousData;
if (!data?.estimatePosition?.liquidation) {
return <span>-</span>;
}
return <span data-testid="liquidation-price">{value}</span>;
let bestCase = '-';
let worstCase = '-';
bestCase =
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
/\..*/,
''
);
worstCase =
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
/\..*/,
''
);
worstCase = addDecimalsFormatNumber(worstCase, marketDecimalPlaces);
bestCase = addDecimalsFormatNumber(bestCase, marketDecimalPlaces);
return (
<Tooltip
description={
<table>
<tbody>
<tr>
<th>{t('Worst case')}</th>
<td className="text-right font-mono pl-2">{worstCase}</td>
</tr>
<tr>
<th>{t('Best case')}</th>
<td className="text-right font-mono pl-2">{bestCase}</td>
</tr>
</tbody>
</table>
}
>
<span data-testid="liquidation-price">{worstCase}</span>
</Tooltip>
);
};
@@ -2,7 +2,12 @@ import * as Schema from '@vegaprotocol/types';
import type { Account } from '@vegaprotocol/accounts';
import type { MarketWithData } from '@vegaprotocol/markets';
import type { PositionFieldsFragment } from './__generated__/Positions';
import { getMetrics, rejoinPositionData } from './positions-data-providers';
import type { Position } from './positions-data-providers';
import {
getMetrics,
preparePositions,
rejoinPositionData,
} from './positions-data-providers';
import { PositionStatus } from '@vegaprotocol/types';
const accounts = [
@@ -180,12 +185,12 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[0].currentLeverage).toBeCloseTo(1.02);
expect(metrics[0].marketDecimalPlaces).toEqual(5);
expect(metrics[0].positionDecimalPlaces).toEqual(0);
expect(metrics[0].decimals).toEqual(5);
expect(metrics[0].assetDecimals).toEqual(5);
expect(metrics[0].markPrice).toEqual('9431775');
expect(metrics[0].marketId).toEqual(
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
);
expect(metrics[0].marketName).toEqual('AAVEDAI.MF21');
expect(metrics[0].marketCode).toEqual('AAVEDAI.MF21');
expect(metrics[0].marketTradingMode).toEqual(
'TRADING_MODE_MONITORING_AUCTION'
);
@@ -205,12 +210,12 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].currentLeverage).toBeCloseTo(0.097);
expect(metrics[1].marketDecimalPlaces).toEqual(5);
expect(metrics[1].positionDecimalPlaces).toEqual(0);
expect(metrics[1].decimals).toEqual(5);
expect(metrics[1].assetDecimals).toEqual(5);
expect(metrics[1].markPrice).toEqual('869762');
expect(metrics[1].marketId).toEqual(
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
);
expect(metrics[1].marketName).toEqual('UNIDAI.MF21');
expect(metrics[1].marketCode).toEqual('UNIDAI.MF21');
expect(metrics[1].marketTradingMode).toEqual('TRADING_MODE_CONTINUOUS');
expect(metrics[1].notional).toEqual('86976200');
expect(metrics[1].openVolume).toEqual('-100');
@@ -223,4 +228,29 @@ describe('getMetrics && rejoinPositionData', () => {
);
expect(metrics[1].status).toEqual(positions[1].positionStatus);
});
it('sorts and filters positions', () => {
const createPosition = (override?: Partial<Position>) =>
({
marketState: Schema.MarketState.STATE_ACTIVE,
marketCode: 'a',
...override,
} as Position);
const data = [
createPosition(),
createPosition({
marketCode: 'c',
marketState: Schema.MarketState.STATE_CANCELLED,
}),
createPosition({ marketCode: 'd' }),
createPosition({ marketCode: 'b' }),
];
const withoutClosed = preparePositions(data, false);
expect(withoutClosed.map((p) => p.marketCode)).toEqual(['a', 'b', 'd']);
const withClosed = preparePositions(data, true);
expect(withClosed.map((p) => p.marketCode)).toEqual(['a', 'b', 'c', 'd']);
});
});
@@ -26,21 +26,22 @@ import {
PositionsDocument,
PositionsSubscriptionDocument,
} from './__generated__/Positions';
import type { PositionStatus } from '@vegaprotocol/types';
import type { PositionStatus, ProductType } from '@vegaprotocol/types';
export interface Position {
assetId: string;
assetSymbol: string;
averageEntryPrice: string;
currentLeverage: number | undefined;
decimals: number;
assetDecimals: number;
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
marketId: string;
marketName: string;
marketCode: string;
marketTradingMode: Schema.MarketTradingMode;
marketState: Schema.MarketState;
markPrice: string | undefined;
notional: string | undefined;
openVolume: string;
@@ -51,6 +52,7 @@ export interface Position {
totalBalance: string;
unrealisedPNL: string;
updatedAt: string | null;
productType: ProductType;
}
export const getMetrics = (
@@ -70,15 +72,10 @@ export const getMetrics = (
const marginAccount = accounts?.find((account) => {
return account.market?.id === market?.id;
});
const {
decimals,
id: assetId,
symbol: assetSymbol,
quantum,
} = market.tradableInstrument.instrument.product.settlementAsset;
const asset = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find(
(account) =>
account.asset.id === assetId &&
account.asset.id === asset.id &&
account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
);
@@ -88,11 +85,11 @@ export const getMetrics = (
const marginAccountBalance = toBigNum(
marginAccount?.balance ?? 0,
decimals
asset.decimals
);
const generalAccountBalance = toBigNum(
generalAccount?.balance ?? 0,
decimals
asset.decimals
);
const markPrice = marketData
@@ -111,18 +108,19 @@ export const getMetrics = (
: notional.dividedBy(totalBalance)
: undefined;
metrics.push({
assetId,
assetSymbol,
assetId: asset.id,
assetSymbol: asset.symbol,
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals,
quantum,
assetDecimals: asset.decimals,
quantum: asset.quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
marketId: market.id,
marketName: market.tradableInstrument.instrument.code,
marketCode: market.tradableInstrument.instrument.code,
marketTradingMode: market.tradingMode,
marketState: market.state,
markPrice: marketData ? marketData.markPrice : undefined,
notional: notional
? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
@@ -132,9 +130,11 @@ export const getMetrics = (
positionDecimalPlaces,
realisedPNL: position.realisedPNL,
status: position.positionStatus,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(),
unrealisedPNL: position.unrealisedPNL,
updatedAt: position.updatedAt || null,
productType: market?.tradableInstrument.instrument.product
.__typename as ProductType,
});
});
return metrics;
@@ -250,6 +250,26 @@ export const rejoinPositionData = (
return null;
};
export const preparePositions = (metrics: Position[], showClosed: boolean) => {
return sortBy(metrics, 'marketCode').filter((p) => {
if (showClosed) {
return true;
}
if (
[
Schema.MarketState.STATE_ACTIVE,
Schema.MarketState.STATE_PENDING,
Schema.MarketState.STATE_SUSPENDED,
].includes(p.marketState)
) {
return true;
}
return false;
});
};
export const positionsMarketsProvider = makeDerivedDataProvider<
string[],
never,
@@ -267,7 +287,7 @@ export const positionsMarketsProvider = makeDerivedDataProvider<
export const positionsMetricsProvider = makeDerivedDataProvider<
Position[],
Position[],
PositionsQueryVariables & { marketIds: string[] }
PositionsQueryVariables & { marketIds: string[]; showClosed: boolean }
>(
[
(callback, client, variables) =>
@@ -283,10 +303,10 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
marketIds: variables.marketIds,
}),
],
([positions, accounts, marketsData]) => {
([positions, accounts, marketsData], variables) => {
const positionsData = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsData, accounts as Account[] | null);
return sortBy(metrics, 'marketName');
return preparePositions(metrics, variables.showClosed);
},
(data, delta, previousData) =>
data.filter((row) => {
+5 -3
View File
@@ -15,7 +15,8 @@ interface PositionsManagerProps {
partyIds: string[];
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
gridProps: ReturnType<typeof useDataGridEvents>;
gridProps?: ReturnType<typeof useDataGridEvents>;
showClosed?: boolean;
}
export const PositionsManager = ({
@@ -23,6 +24,7 @@ export const PositionsManager = ({
onMarketClick,
isReadOnly,
gridProps,
showClosed = false,
}: PositionsManagerProps) => {
const { pubKeys, pubKey } = useVegaWallet();
const create = useVegaTransactionStore((store) => store.create);
@@ -60,7 +62,7 @@ export const PositionsManager = ({
const { data, error } = useDataProvider({
dataProvider: positionsMetricsProvider,
variables: { partyIds, marketIds: marketIds || [] },
variables: { partyIds, marketIds: marketIds || [], showClosed },
skip: !marketIds,
});
@@ -68,7 +70,7 @@ export const PositionsManager = ({
<PositionsTable
pubKey={pubKey}
pubKeys={pubKeys}
rowData={error ? [] : data}
rowData={data}
onMarketClick={onMarketClick}
onClose={onClose}
isReadOnly={isReadOnly}
+177 -186
View File
@@ -1,10 +1,9 @@
import type { RenderResult } from '@testing-library/react';
import { act, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PositionsTable, { OpenVolumeCell, PNLCell } from './positions-table';
import { PositionsTable, OpenVolumeCell, PNLCell } from './positions-table';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { PositionStatus } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
@@ -20,14 +19,15 @@ const singleRow: Position = {
assetSymbol: 'BTC',
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2, // this is settlementAsset.decimals
assetDecimals: 2, // this is settlementAsset.decimals
quantum: '0.1',
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
marketId: 'string',
marketName: 'ETH/BTC (31 july 2022)',
marketCode: 'ETHBTC.QM21',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
marketState: Schema.MarketState.STATE_ACTIVE,
markPrice: '123',
notional: '12300',
openVolume: '100',
@@ -37,11 +37,16 @@ const singleRow: Position = {
totalBalance: '123456',
unrealisedPNL: '456',
updatedAt: '2022-07-27T15:02:58.400Z',
productType: 'Future',
};
const singleRowData = [singleRow];
describe('Positions', () => {
const renderComponent = async (rowData: Position) => {
await act(async () => {
render(<PositionsTable rowData={[rowData]} isReadOnly={false} />);
});
};
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(
@@ -52,157 +57,132 @@ describe('Positions', () => {
});
it('render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(11);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
const expectedHeaders = [
'Market',
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Asset',
'Entry price',
'Leverage',
'Size / Notional',
'Entry / Mark',
'Margin',
'Liquidation',
'Realised PNL',
'Unrealised PNL',
]);
];
await renderComponent(singleRow);
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(expectedHeaders.length);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual(expectedHeaders);
});
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
it('renders market code', async () => {
await renderComponent(singleRow);
expect(screen.getByText(singleRow.marketCode)).toBeTruthy();
expect(screen.getByText('Futr')).toBeInTheDocument();
});
it('Does not fail if the market name does not match the split pattern', async () => {
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
const row = [
Object.assign({}, singleRow, { marketName: breakingMarketName }),
];
await act(async () => {
render(<PositionsTable rowData={row} isReadOnly={false} />);
});
await renderComponent({ ...singleRow, marketCode: breakingMarketName });
expect(screen.getByText(breakingMarketName)).toBeTruthy();
});
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
it('displays size / notional correctly for long position', async () => {
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '-100' }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('1,230.0');
expect(cell).toHaveClass('text-market-green-600');
expect(cell).not.toHaveClass('text-market-red');
expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent(
'+100'
);
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
'1,230.0'
);
});
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
it('displays size / notional correctly for short position', async () => {
await renderComponent({ ...singleRow, openVolume: '-100' });
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
expect(cell).not.toHaveClass('text-market-green-600');
expect(cell).toHaveClass('text-market-red');
expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent(
'-100'
);
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
'1,230.0'
);
});
it('displays entry / mark price', async () => {
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
const cell = within(cells[2]);
expect(cell.getByTestId('stack-cell-primary')).toHaveTextContent('13.3');
expect(cell.getByTestId('stack-cell-secondary')).toHaveTextContent('12.3');
});
it('doesnt render entry / mark if market is in opening auction', async () => {
await renderComponent({
...singleRow,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
});
let cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[
{
...singleRow,
marketTradingMode:
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
},
]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('-');
const cells = screen.getAllByRole('gridcell');
expect(cells[2].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
it('displays margin and leverage', async () => {
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
// margin
expect(
within(cells[3]).getByTestId('stack-cell-primary')
).toHaveTextContent('123,456.00');
// leverage
expect(
within(cells[3]).getByTestId('stack-cell-secondary')
).toHaveTextContent('1.1');
});
it('displays realised and unrealised PNL', async () => {
// pnl cells should be rendered with asset dps
const expectedRealised = addDecimalsFormatNumber(
singleRow.realisedPNL,
singleRow.decimals
singleRow.assetDecimals
);
const expectedUnrealised = addDecimalsFormatNumber(
singleRow.unrealisedPNL,
singleRow.decimals
singleRow.assetDecimals
);
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual(expectedRealised);
expect(cells[10].textContent).toEqual(expectedUnrealised);
expect(cells[5]).toHaveTextContent(expectedRealised);
expect(cells[6]).toHaveTextContent(expectedUnrealised);
});
it('displays close button', async () => {
await act(async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
rowData={[singleRow]}
pubKey={singleRow.partyId}
onClose={() => {
return;
}}
@@ -210,24 +190,15 @@ describe('Positions', () => {
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[11].textContent).toEqual('Close');
expect(screen.getByTestId('close-position')).toBeInTheDocument();
});
it('do not display close button if openVolume is zero', async () => {
await act(async () => {
render(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '0' }]}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[11].textContent).toEqual('');
await renderComponent({ ...singleRow, openVolume: '0' });
expect(
screen.queryByRole('button', { name: 'Close' })
).not.toBeInTheDocument();
});
describe('PNLCell', () => {
@@ -247,40 +218,27 @@ describe('Positions', () => {
lossSocialisationAmount: '0',
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
} as ICellRendererParams;
render(<PNLCell {...props} />);
expect(
screen.getByText(props.valueFormatted as string)
).toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
});
it('renders value with warning tooltip if loss socialisation occurred', async () => {
it('renders value with warning icon if loss socialisation occurred', () => {
const props = {
data: {
...singleRow,
lossSocializationAmount: '500',
decimals: 2,
assetDecimals: 2,
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'Lifetime loss socialisation deductions: 5.00'
)
).toBeInTheDocument();
expect(
within(tooltip).getByText(
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
)
).toBeInTheDocument();
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
});
@@ -288,9 +246,10 @@ describe('Positions', () => {
const props = {
data: undefined,
valueFormatted: '100',
};
} as ICellRendererParams;
it('renders a dash if no data', () => {
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
render(<OpenVolumeCell {...props} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
@@ -304,36 +263,21 @@ describe('Positions', () => {
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
});
it('renders status with warning tooltip if orders were closed', async () => {
it('renders status with warning tooltip if orders were closed', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
} as ICellRendererParams;
render(<OpenVolumeCell {...props} />);
const content = screen.getByText(props.valueFormatted as string);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
)
).toBeInTheDocument();
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
it('renders status with warning tooltip if position was closed out', async () => {
@@ -343,24 +287,71 @@ describe('Positions', () => {
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
} as ICellRendererParams;
render(<OpenVolumeCell {...props} />);
const content = screen.getByText(props.valueFormatted as string);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
});
describe('position status from size column', () => {
it('does not show if position status is normal', async () => {
await renderComponent({
...singleRow,
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
await userEvent.hover(cell);
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
it.each([
{
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
text: 'Your position was closed.',
},
{
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
text: 'Your open orders were cancelled.',
},
{
status: PositionStatus.POSITION_STATUS_DISTRESSED,
text: 'Your position is distressed.',
},
])('renders content for $status', async (data) => {
await renderComponent({
...singleRow,
status: data.status,
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
await userEvent.hover(cell);
const tooltip = within(await screen.findByRole('tooltip'));
expect(tooltip.getByText(data.text)).toBeInTheDocument();
});
});
describe('loss socialization from realised pnl column', () => {
it('renders', async () => {
await renderComponent({
...singleRow,
lossSocializationAmount: '500',
assetDecimals: 2,
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[5];
await userEvent.hover(cell);
const tooltip = within(await screen.findByRole('tooltip'));
expect(tooltip.getByText('Realised PNL: 1.23')).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
tooltip.getByText('Lifetime loss socialisation deductions: 5.00')
).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'You did not have enough BTC collateral to meet the maintenance margin requirements for your position, so it was closed by the network.'
tooltip.getByText(
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
)
).toBeInTheDocument();
});
+310 -308
View File
@@ -1,44 +1,46 @@
import classNames from 'classnames';
import { useMemo } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { ColDef } from 'ag-grid-community';
import type { ColDef, ITooltipParams } from 'ag-grid-community';
import type {
VegaValueFormatterParams,
VegaValueGetterParams,
TypedDataAgGrid,
VegaICellRendererParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import { ProgressBarCell } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
COL_DEFS,
PriceFlashCell,
signedNumberCssClass,
signedNumberCssClassRules,
MarketNameCell,
ProgressBarCell,
MarketProductPill,
} from '@vegaprotocol/datagrid';
import {
ButtonLink,
Tooltip,
TooltipCellComponent,
ExternalLink,
Icon,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
volumePrefix,
toBigNum,
formatNumber,
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import {
MarketTradingMode,
PositionStatus,
PositionStatusMapping,
} from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionActionsDropdown } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { LiquidationPrice } from './liquidation-price';
import { StackedCell } from './stacked-cell';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -46,44 +48,25 @@ interface Props extends TypedDataAgGrid<Position> {
style?: CSSProperties;
isReadOnly: boolean;
multipleKeys?: boolean;
pubKeys?: VegaWalletContextShape['pubKeys'];
pubKey?: VegaWalletContextShape['pubKey'];
pubKeys?: Array<{ name: string; publicKey: string }> | null;
pubKey?: string | null;
}
export interface AmountCellProps {
valueFormatted?: Pick<
Position,
'openVolume' | 'marketDecimalPlaces' | 'positionDecimalPlaces' | 'notional'
>;
}
export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
if (!valueFormatted) {
return null;
}
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
valueFormatted;
return valueFormatted && notional ? (
<div className="leading-tight font-mono">
<div
className={classNames('text-right', signedNumberCssClass(openVolume))}
>
{volumePrefix(
addDecimalsFormatNumber(openVolume, positionDecimalPlaces)
)}
</div>
<div className="text-right">
{addDecimalsFormatNumber(notional, marketDecimalPlaces)}
</div>
</div>
) : null;
};
AmountCell.displayName = 'AmountCell';
export const getRowId = ({ data }: { data: Position }) =>
`${data.partyId}-${data.marketId}`;
const realisedPNLValueGetter = ({ data }: { data: Position }) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.assetDecimals).toNumber();
};
const unrealisedPNLValueGetter = ({ data }: { data: Position }) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.assetDecimals).toNumber();
};
const defaultColDef = {
sortable: true,
filter: true,
@@ -101,7 +84,6 @@ export const PositionsTable = ({
pubKey,
...props
}: Props) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return (
<AgGrid
overlayNoRowsTemplate={t('No positions')}
@@ -109,11 +91,11 @@ export const PositionsTable = ({
tooltipShowDelay={500}
defaultColDef={defaultColDef}
components={{
AmountCell,
PriceFlashCell,
ProgressBarCell,
MarketNameCell,
}}
rowHeight={45}
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
@@ -130,41 +112,37 @@ export const PositionsTable = ({
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
},
{
headerName: t('Notional'),
headerTooltip: t('Mark price x open volume.'),
field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
field: 'marketCode',
onCellClicked: ({ data }) => {
if (!onMarketClick) return;
onMarketClick(data.marketId);
},
valueFormatter: ({
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
}: VegaICellRendererParams<Position, 'marketCode'>) => {
if (!data || !value) return '-';
return (
<StackedCell
primary={value}
secondary={
<>
{data?.assetSymbol}
<MarketProductPill productType={data.productType} />
</>
}
/>
);
},
},
{
headerName: t('Open volume'),
headerName: t('Size / Notional'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
valueGetter: ({ data }: { data: Position }) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
@@ -172,165 +150,205 @@ export const PositionsTable = ({
data.positionDecimalPlaces
).toNumber();
},
tooltipValueGetter: ({ data }: ITooltipParams<Position>) => {
if (
!data ||
data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED
) {
return null;
}
return data.status;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
if (!data?.openVolume) return '-';
const vol = volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
return vol;
},
tooltipComponent: (args: ITooltipParams<Position>) => {
if (!args.data) {
return null;
}
const POSITION_RESOLUTION_LINK =
DocsLinks?.POSITION_RESOLUTION ?? '';
let primaryTooltip;
switch (args.data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
}
let secondaryTooltip;
switch (args.data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
args.data.assetSymbol
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
return (
<TooltipCellComponent
{...args}
value={
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t(
'Status: %s',
PositionStatusMapping[args.data.status]
)}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
{t('Read more about position resolution')}
</ExternalLink>
)}
</>
}
/>
);
},
cellRenderer: OpenVolumeCell,
},
{
headerName: t('Mark price'),
headerName: t('Entry / Mark'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
cellClass: 'font-mono text-right',
cellRenderer: ({
data,
}: VegaICellRendererParams<Position, 'markPrice'>) => {
if (
!data?.averageEntryPrice ||
!data?.markPrice ||
!data?.marketDecimalPlaces
) {
return <>-</>;
}
if (
data.marketTradingMode ===
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return <>-</>;
}
const entry = addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
const mark = addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
return (
<StackedCell
primary={entry}
secondary={
<PriceFlashCell
value={Number(data.markPrice)}
valueFormatted={mark}
/>
}
/>
);
},
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
},
{
headerName: t('Margin'),
colId: 'margin',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.assetDecimals
).toNumber();
},
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
!data ||
!data.marginAccountBalance ||
!data.marketDecimalPlaces
) {
return '-';
return null;
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
const margin = addDecimalsFormatNumberQuantum(
data.marginAccountBalance,
data.assetDecimals,
data.quantum
);
const lev = data?.currentLeverage ? data.currentLeverage : 1;
const leverage = formatNumber(Math.max(1, lev), 1);
return (
<StackedCell primary={margin} secondary={leverage + 'x'} />
);
},
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
headerName: 'Liquidation',
headerTooltip: t('Worst case liquidation price'),
cellClass: 'font-mono text-right',
type: 'rightAligned',
// Cannot be sortable as data is fetched within the cell
sortable: false,
filter: false,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
if (!data) {
return '-';
}
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
marketDecimalPlaces={data.marketDecimalPlaces}
/>
);
},
},
{
headerName: t('Asset'),
field: 'assetSymbol',
colId: 'asset',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
title={t('View settlement asset details')}
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
},
},
{
headerName: t('Entry price'),
field: 'averageEntryPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? '' : formatNumber(value.toString(), 1),
},
multipleKeys
? null
: {
headerName: t('Margin'),
field: 'marginAccountBalance',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.decimals
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
},
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
@@ -338,17 +356,71 @@ export const PositionsTable = ({
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
valueGetter: realisedPNLValueGetter,
// @ts-ignore no type overlap, but the functions are identical
tooltipValueGetter: realisedPNLValueGetter,
tooltipComponent: (args: ITooltipParams) => {
const LOSS_SOCIALIZATION_LINK =
DocsLinks?.LOSS_SOCIALIZATION ?? '';
if (!args.data) {
return <>-</>;
}
const losses = parseInt(
args.data?.lossSocializationAmount ?? '0'
);
if (losses <= 0) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{args.valueFormatted}</>;
}
const lossesFormatted = addDecimalsFormatNumber(
args.data.lossSocializationAmount,
args.data.assetDecimals
);
return (
<TooltipCellComponent
{...args}
value={
<>
<p className="mb-2">
{t('Realised PNL: %s', args.value)}
</p>
<p className="mb-2">
{t(
'Lifetime loss socialisation deductions: %s',
lossesFormatted
)}
</p>
<p className="mb-2">
{t(
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
args.data.assetSymbol
)}
</p>
{LOSS_SOCIALIZATION_LINK && (
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
{t('Read more about loss socialisation')}
</ExternalLink>
)}
</>
}
/>
);
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
: addDecimalsFormatNumberQuantum(
data.realisedPNL,
data.assetDecimals,
data.quantum
);
},
headerTooltip: t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
@@ -362,21 +434,22 @@ export const PositionsTable = ({
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueGetter: unrealisedPNLValueGetter,
// @ts-ignore no type overlap but function can be identical
tooltipValueGetter: unrealisedPNLValueGetter,
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
: addDecimalsFormatNumberQuantum(
data.unrealisedPNL,
data.assetDecimals,
data.quantum
),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
},
onClose && !isReadOnly
? {
@@ -391,7 +464,7 @@ export const PositionsTable = ({
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
<VegaIcon name={VegaIconNames.CROSS} size={16} />
</ButtonLink>
) : null}
{data?.assetId && (
@@ -400,36 +473,24 @@ export const PositionsTable = ({
</div>
);
},
minWidth: 75,
maxWidth: 75,
minWidth: 55,
maxWidth: 55,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
}, [isReadOnly, multipleKeys, onClose, onMarketClick, pubKey, pubKeys])}
{...props}
/>
);
};
export default PositionsTable;
export const PNLCell = ({
valueFormatted,
data,
}: VegaICellRendererParams<Position, 'realisedPNL'>) => {
const LOSS_SOCIALIZATION_LINK = DocsLinks?.LOSS_SOCIALIZATION ?? '';
if (!data) {
return <>-</>;
}
@@ -440,121 +501,62 @@ export const PNLCell = ({
return <>{valueFormatted}</>;
}
const lossesFormatted = addDecimalsFormatNumber(
data.lossSocializationAmount,
data.decimals
);
return (
<WarningCell
tooltipContent={
<>
<p className="mb-2">
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
</p>
<p className="mb-2">
{t(
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
[data.assetSymbol]
)}
</p>
{LOSS_SOCIALIZATION_LINK && (
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
{t('Read more about loss socialisation')}
</ExternalLink>
)}
</>
}
>
{valueFormatted}
</WarningCell>
);
return <WarningCell>{valueFormatted}</WarningCell>;
};
export const OpenVolumeCell = ({
valueFormatted,
data,
}: VegaICellRendererParams<Position, 'openVolume'>) => {
if (!data) {
if (!valueFormatted || !data || !data.notional) {
return <>-</>;
}
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
const notional = addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
let primaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
const cellContent = (
<StackedCell primary={valueFormatted} secondary={notional} />
);
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{cellContent}</>;
}
let secondaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
[data.assetSymbol]
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
return (
<WarningCell
showIcon={data.status !== PositionStatus.POSITION_STATUS_UNSPECIFIED}
tooltipContent={
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t('Status: %s', PositionStatusMapping[data.status])}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
{t('Read more about position resolution')}
</ExternalLink>
)}
</>
showIcon={
// not sure why but data.status has become a union of all the enum values
// rather than just being the enum itself
(data.status as PositionStatus) !==
PositionStatus.POSITION_STATUS_UNSPECIFIED
}
>
{valueFormatted}
{cellContent}
</WarningCell>
);
};
const WarningCell = ({
children,
tooltipContent,
showIcon = true,
}: {
children: ReactNode;
tooltipContent: ReactNode;
showIcon?: boolean;
}) => {
return (
<Tooltip description={tooltipContent}>
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
<span className="text-black dark:text-white mr-1">
{showIcon && <Icon name="warning-sign" size={3} />}
<div className="flex justify-end items-center">
{showIcon && (
<span className="text-black dark:text-white mr-2">
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />
</span>
<span className="text-ellipsis overflow-hidden">{children}</span>
</div>
</Tooltip>
)}
<span className="overflow-hidden whitespace-nowrap text-ellipsis">
{children}
</span>
</div>
);
};
+18
View File
@@ -0,0 +1,18 @@
import type { ReactNode } from 'react';
export const StackedCell = ({
primary,
secondary,
}: {
primary: ReactNode;
secondary: ReactNode;
}) => {
return (
<div className="leading-4 text-ellipsis whitespace-nowrap overflow-hidden">
<div data-testid="stack-cell-primary">{primary}</div>
<div data-testid="stack-cell-secondary" className="text-muted">
{secondary}
</div>
</div>
);
};
+3 -2
View File
@@ -1,6 +1,7 @@
export * from './asset-proposal-notification';
export * from './market-proposal-notification';
export * from './protocol-upgrade-countdown';
export * from './protocol-upgrade-proposal-notification';
export * from './proposals-list';
export * from './protocol-upgrade-countdown';
export * from './protocol-upgrade-in-progress-notification';
export * from './protocol-upgrade-proposal-notification';
export * from './voting-progress';
@@ -2,7 +2,7 @@ import React from 'react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
import { useColumnDefs } from './use-column-defs';
import { MarketNameProposalCell, useColumnDefs } from './use-column-defs';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
@@ -43,7 +43,7 @@ export const ProposalsList = ({
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer }}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
/>
</div>
);
@@ -19,14 +19,51 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ProposalStateMapping } from '@vegaprotocol/types';
import { ExternalLink, Pill } from '@vegaprotocol/ui-toolkit';
import {
ProposalProductTypeMapping,
ProposalProductTypeShortName,
ProposalStateMapping,
} from '@vegaprotocol/types';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { VoteProgress } from '../voting-progress';
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
export const useColumnDefs = () => {
export const MarketNameProposalCell = ({
value,
data,
}: VegaICellRendererParams<
ProposalListFieldsFragment,
'terms.change.instrument.code'
>) => {
const { VEGA_TOKEN_URL } = useEnvironment();
const { change } = data?.terms || {};
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
const type = change.instrument.futureProduct?.__typename;
const content = (
<>
<span data-testid="market-code">{value as string}</span>
{type && (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProposalProductTypeMapping[type]}
>
{ProposalProductTypeShortName[type]}
</Pill>
)}
</>
);
if (data?.id) {
const link = `${VEGA_TOKEN_URL}/proposals/${data.id}`;
return <ExternalLink href={link}>{content}</ExternalLink>;
}
return content;
}
return null;
};
export const useColumnDefs = () => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
@@ -43,26 +80,7 @@ export const useColumnDefs = () => {
headerName: t('Market'),
field: 'terms.change.instrument.code',
cellStyle: { lineHeight: '14px' },
cellRenderer: ({
data,
}: VegaICellRendererParams<
ProposalListFieldsFragment,
'terms.change.instrument.code'
>) => {
const { change } = data?.terms || {};
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
if (data?.id) {
const link = `${VEGA_TOKEN_URL}/proposals/${data.id}`;
return (
<ExternalLink href={link}>
{change.instrument.code}
</ExternalLink>
);
}
return change.instrument.code;
}
return null;
},
cellRenderer: 'MarketNameProposalCell',
},
{
colId: 'description',
@@ -155,7 +173,7 @@ export const useColumnDefs = () => {
},
},
]);
}, [VEGA_TOKEN_URL, requiredMajorityPercentage]);
}, [requiredMajorityPercentage]);
const defaultColDef: ColDef = useMemo(() => {
return {
@@ -0,0 +1,77 @@
import { useProtocolUpgradeProposalLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import {
ExternalLink,
Intent,
NotificationBanner,
SHORT,
} from '@vegaprotocol/ui-toolkit';
import type { StoredNextProtocolUpgradeData } from '../lib';
import {
NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT,
useNextProtocolUpgradeProposal,
} from '../lib';
import { useLocalStorageSnapshot } from '@vegaprotocol/react-helpers';
import { useBlockRising } from '../lib/protocol-upgrade-proposals/use-block-rising';
/**
* A flag determining whether to get the upgrade proposal data from local
* storage if `useNextProtocolUpgradeProposal` fails.
*/
const ALLOW_STORED_PROPOSAL_DATA = true;
export const ProtocolUpgradeInProgressNotification = () => {
const { data, error } = useNextProtocolUpgradeProposal(undefined, true);
const [nextUpgrade] = useLocalStorageSnapshot(
NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT
);
const { blocksRising, block } = useBlockRising();
const detailsLink = useProtocolUpgradeProposalLink();
let vegaReleaseTag: string | undefined;
let upgradeBlockHeight: string | undefined;
if (error && !data && nextUpgrade && ALLOW_STORED_PROPOSAL_DATA) {
try {
const stored = JSON.parse(nextUpgrade) as StoredNextProtocolUpgradeData;
vegaReleaseTag = stored.vegaReleaseTag;
upgradeBlockHeight = stored.upgradeBlockHeight;
} catch {
// no op
}
}
/**
* If upgrade is in progress then none of the nodes should produce blocks,
* same should be with the tendermint block info otherwise it's a network
* issue and not an upgrade. The upgrade usually lasts for couple of minutes.
*
* Once the networks is back then the notification disappears.
*/
const upgradeInProgress =
vegaReleaseTag &&
upgradeBlockHeight &&
!blocksRising &&
block <= Number(upgradeBlockHeight);
if (!upgradeInProgress) return null;
return (
<NotificationBanner intent={Intent.Danger} className={SHORT}>
<div className="uppercase">
{t('The network is being upgraded to %s', vegaReleaseTag)}
</div>
<div>
{t(
'Trading and other network activity has stopped until the upgrade is complete.'
)}{' '}
{vegaReleaseTag && (
<ExternalLink href={detailsLink(vegaReleaseTag)}>
{t('View details')}
</ExternalLink>
)}
</div>
</NotificationBanner>
);
};
@@ -29,27 +29,32 @@ export const ProtocolUpgradeProposalNotification = ({
const { vegaReleaseTag, upgradeBlockHeight } = data;
let timeLeft = time;
let blocksLeft = Number(upgradeBlockHeight) - Number(lastBlockHeight);
if (blocksLeft < 0) {
blocksLeft = 0;
timeLeft = 0;
}
let countdown;
switch (mode) {
case ProtocolUpgradeCountdownMode.IN_BLOCKS:
countdown = (
<>
<span className="text-vega-orange-500">
{Number(upgradeBlockHeight) - Number(lastBlockHeight)}
</span>{' '}
<span className="text-vega-orange-500">{blocksLeft}</span>{' '}
{t('blocks')}
</>
);
break;
case ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING:
countdown =
time !== undefined ? (
timeLeft !== undefined ? (
<span
title={t('estimated time to protocol upgrade')}
className="text-vega-orange-500"
data-testid="upgrade-proposal-estimate"
>
{convertToCountdownString(time, '0:00:00:00')}
{convertToCountdownString(timeLeft, '0:00:00:00')}
</span>
) : (
<span className="text-vega-orange-600 lowercase italic">
@@ -1,3 +1,4 @@
export * from './__generated__/ProtocolUpgradeProposals';
export * from './use-block-rising';
export * from './use-next-protocol-upgrade-proposals';
export * from './use-time-to-upgrade';
@@ -0,0 +1,92 @@
import { createClient } from '@vegaprotocol/apollo-client';
import { useEnvironment } from '@vegaprotocol/environment';
import { useBlockInfo } from '@vegaprotocol/tendermint';
import { useEffect, useMemo, useState } from 'react';
import type { BlockStatisticsQuery } from './__generated__/BlockStatistics';
import { BlockStatisticsDocument } from './__generated__/BlockStatistics';
import compact from 'lodash/compact';
import max from 'lodash/max';
const CHECK_INTERVAL = 5000; // ms
/**
* Allows stale block height for given number of times. If stale count is
* greater than this number then it is marked as not rising (producing blocks).
*/
const ALLOW_STALE = 2; // times -> MAX(this, 1) * CHECK_INTERVAL ~> min check time
export const useBlockRising = () => {
const [blocksRising, setBlocksRising] = useState(true);
const [block, setBlock] = useState(0);
const nodes = useEnvironment((state) => state.nodes);
const clients = useMemo(() => {
return nodes.map(
(n) =>
n &&
n.length > 0 &&
createClient({
url: n,
cacheConfig: undefined,
retry: false,
connectToDevTools: false,
connectToHeaderStore: true,
})
);
}, [nodes]);
const { refetch: fetchBlockInfo } = useBlockInfo();
useEffect(() => {
let stale = 0;
let prev = 0;
const check = async () => {
const queries = clients.map((client, index) =>
client
? client
.query<BlockStatisticsQuery>({
query: BlockStatisticsDocument,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
})
.catch((err) =>
Promise.reject(
`could not retrieve statistics from ${nodes[index]}`
)
)
: undefined
);
const blockInfo = await fetchBlockInfo();
const results = (await Promise.allSettled(compact(queries))).map(
(res) => {
if (res && res.status === 'fulfilled') {
return res.value.data.statistics;
} else {
return undefined;
}
}
);
const heights = compact([
...results.map((r) => r?.blockHeight),
blockInfo?.result.block.header.height,
]);
const current = max(heights);
if (current && Number(current) > prev) {
setBlock(Number(current));
prev = Number(current);
stale = 0;
if (!blocksRising) setBlocksRising(true);
} else {
if (stale > ALLOW_STALE) setBlocksRising(false);
stale++;
}
};
const interval = setInterval(check, CHECK_INTERVAL);
check();
return () => {
clearInterval(interval);
};
}, [clients, fetchBlockInfo, blocksRising, nodes]);
return { blocksRising, block };
};
@@ -2,6 +2,13 @@ import { useMemo, useEffect } from 'react';
import * as Schema from '@vegaprotocol/types';
import { removePaginationWrapper, isTestEnv } from '@vegaprotocol/utils';
import { useProtocolUpgradeProposalsQuery } from './__generated__/ProtocolUpgradeProposals';
import { useLocalStorageSnapshot } from '@vegaprotocol/react-helpers';
import pick from 'lodash/pick';
const POLL_INTERVAL = 5000; // ms
export const NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT =
'next_protocol_upgrade_proposal';
export const useNextProtocolUpgradeProposals = (since?: number) => {
const { data, loading, error, startPolling, stopPolling } =
@@ -22,7 +29,7 @@ export const useNextProtocolUpgradeProposals = (since?: number) => {
}
if (!isTestEnv() && window.location.hostname !== 'localhost') {
startPolling(5000);
startPolling(POLL_INTERVAL);
}
}, [error, startPolling, stopPolling]);
@@ -51,10 +58,28 @@ export const useNextProtocolUpgradeProposals = (since?: number) => {
};
};
export const useNextProtocolUpgradeProposal = (since?: number) => {
export type StoredNextProtocolUpgradeData = {
vegaReleaseTag: string;
upgradeBlockHeight: string;
};
export const useNextProtocolUpgradeProposal = (
since?: number,
persist = false
) => {
const { data, lastBlockHeight, loading, error } =
useNextProtocolUpgradeProposals(since);
const [, setNextUpgrade] = useLocalStorageSnapshot(
NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT
);
useEffect(() => {
if (data && persist) {
setNextUpgrade(
JSON.stringify(pick(data, 'upgradeBlockHeight', 'vegaReleaseTag'))
);
}
}, [data, persist, setNextUpgrade]);
return {
data: !data ? undefined : data[0],
lastBlockHeight,
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "tendermint",
"name": "@vegaprotocol/tendermint",
"version": "0.0.1"
}
+1 -1
View File
@@ -5,7 +5,7 @@ import type { TendermintBlockResponse } from '../types';
export const useBlockInfo = (blockHeight?: number, canFetch = true) => {
const { TENDERMINT_URL } = useEnvironment();
const url = `${TENDERMINT_URL}/block?height=${blockHeight}`;
const url = `${TENDERMINT_URL}/block?height=${blockHeight || ''}`;
const canFetchData = Boolean(
TENDERMINT_URL && blockHeight && !isNaN(blockHeight) && canFetch
);
+18
View File
@@ -25,6 +25,7 @@ import type {
DispatchMetric,
StopOrderStatus,
} from './__generated__/types';
import type { ProductType, ProposalProductType } from './product';
export const AccountTypeMapping: {
[T in AccountType]: string;
@@ -513,3 +514,20 @@ export const PeggedReferenceMapping: { [R in PeggedReference]: string } = {
PEGGED_REFERENCE_BEST_BID: 'Bid',
PEGGED_REFERENCE_MID: 'Mid',
};
export const ProductTypeMapping: Record<ProductType, string> = {
Future: 'Future',
};
export const ProductTypeShortName: Record<ProductType, string> = {
Future: 'Futr',
};
export const ProposalProductTypeMapping: Record<ProposalProductType, string> = {
FutureProduct: 'Future',
};
export const ProposalProductTypeShortName: Record<ProposalProductType, string> =
{
FutureProduct: 'Futr',
};
+1
View File
@@ -1,3 +1,4 @@
export * from './__generated__/types';
export * from './candle';
export * from './global-types-mappings';
export * from './product';
+7
View File
@@ -0,0 +1,7 @@
import type { Product } from './__generated__/types';
export type ProductType = NonNullable<Product['__typename']>;
// TODO: Update to be dynamically created for ProductionConfiguration union when schema
// changes make it to stagnet1
export type ProposalProductType = 'FutureProduct';
@@ -4,6 +4,8 @@ import { Intent } from '../../utils/intent';
import { Icon } from '../icon';
import type { HTMLAttributes } from 'react';
export const SHORT = '!px-1 !py-1 min-h-fit';
interface NotificationBannerProps {
intent?: Intent;
children?: React.ReactNode;
+20 -11
View File
@@ -1,18 +1,18 @@
import type { ReactNode } from 'react';
import type { ReactNode, HTMLProps } from 'react';
import { Intent } from '../../utils/intent';
import classNames from 'classnames';
type Size = 'lg' | 'md' | 'sm' | 'xs' | 'xxs';
interface Props {
interface Props extends Omit<HTMLProps<HTMLSpanElement>, 'size'> {
children: ReactNode;
intent?: Intent;
size?: Size;
className?: string;
}
const getClasses = (size: Size, intent?: Intent, className?: string) => {
const getClasses = (size: Size, intent: Intent, className?: string) => {
return classNames(
['rounded-md', 'leading-none', 'font-alpha', 'py-1 px-2'],
['rounded-md', 'leading-none', 'font-alpha'],
{
'bg-vega-yellow dark:bg-vega-yellow': intent === Intent.Primary,
'bg-vega-clight-500 dark:bg-vega-cdark-500': intent === Intent.None,
@@ -25,19 +25,28 @@ const getClasses = (size: Size, intent?: Intent, className?: string) => {
intent === Intent.Primary,
},
{
'text-lg': size === 'lg',
'text-base': size === 'md',
'text-sma': size === 'sm',
'text-xs': size === 'xs',
'text-[10px]': size === 'xxs',
'text-lg py-1 px-2': size === 'lg',
'text-base py-1 px-2': size === 'md',
'text-sm py-1 px-1': size === 'sm',
'text-xs py-1 px-1': size === 'xs',
'text-[10px] py-0 px-1': size === 'xxs',
},
className
);
};
export const Pill = ({ intent, size, className, children }: Props) => {
export const Pill = ({
intent,
size,
className,
children,
...props
}: Props) => {
return (
<span className={getClasses(size || 'md', intent, className)}>
<span
className={getClasses(size || 'md', intent || Intent.None, className)}
{...props}
>
{children}
</span>
);
@@ -60,5 +60,9 @@ export const Tooltip = ({
);
export const TooltipCellComponent = (props: ITooltipParams) => {
return <p className={tooltipContentClasses}>{props.value}</p>;
return (
<div className={tooltipContentClasses} role="tooltip">
{props.value}
</div>
);
};
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { NotificationBanner } from '../notification-banner';
import { NotificationBanner, SHORT } from '../notification-banner';
import { Intent } from '../../utils/intent';
import { TradingButton } from '../trading-button';
@@ -25,7 +25,7 @@ export const ViewingAsBanner = ({
<NotificationBanner
data-testid="view-banner"
intent={Intent.None}
className="!px-1 !py-1 min-h-fit"
className={SHORT}
>
<div className="flex justify-between items-baseline">
<span>
+2
View File
@@ -44,6 +44,8 @@ describe('number utils', () => {
o: '12,345,678.91234568',
q: '1',
},
// USDT / USDC
{ v: new BigNumber(12345678), d: 6, o: '12.35', q: 1000000 },
])(
'formats with addDecimalsFormatNumberQuantum given number correctly',
({ v, d, o, q }) => {
+32 -13
View File
@@ -4,30 +4,37 @@
- **Must** be able to view the following columns (<a name="7004-POSI-002" href="#7004-POSI-002">7004-POSI-002</a>):
- market
- notional
- open volume
- mark price
- settlement asset
- entry price
- leverage
- margin allocated
- realised PNL
- unrealised PNL
- Updated
- Market
- Code
- Settlement asset symbol
- Product type
- Size / Notional
- Size
- Notional
- Entry / Mark
- Entry
- Mark
- Margin
- Allocated
- Leverage
- Liquidation
- Realised PNL
- Unrealised PNL
- **Must** be able to sort each column by asc and dsc (<a name="7004-POSI-003" href="#7004-POSI-003">7004-POSI-003</a>)
- If the cell shows two values the primary (top value) is used to sort (<a name="7004-POSI-014" href="#7004-POSI-014">7004-POSI-014</a>)
- **Must** be able to scroll horizontally to see all columns if screen resolution isn't wide enough (<a name="7004-POSI-004" href="#7004-POSI-004">7004-POSI-004</a>)
- **Must** be able to drag and drop column names to re order (<a name="7004-POSI-005" href="#7004-POSI-005">7004-POSI-005</a>)
- **Must** be able to remove columns by dragging headers out of the table (<a name="7004-POSI-015" href="#7004-POSI-015">7004-POSI-015</a>)
- **Must** be able to resize the width of the columns (<a name="7004-POSI-006" href="#7004-POSI-006">7004-POSI-006</a>)
- **Must** The columns 'Open volume', 'realised PNL' and 'Unrealised PNL' must change color and have a + or - suffix depending on being positive/negative (<a name="7004-POSI-007" href="#7004-POSI-007">7004-POSI-007</a>)
- **Must** be able to select settlement asset to view asset details (<a name="7004-POSI-008" href="#7004-POSI-008">7004-POSI-008</a>)
- **Must** be able to select the 3 dots to view asset details (<a name="7004-POSI-009" href="#7004-POSI-009">7004-POSI-009</a>)
- **Must** be able to select the close button to close my position (<a name="7004-POSI-010" href="#7004-POSI-010">7004-POSI-010</a>)
@@ -37,3 +44,15 @@
- **Must** retain previous column sizing on refresh (<a name="7004-POSI-012" href="#7004-POSI-012">7004-POSI-012</a>)
- **Must** notional column has a tooltip on hover (<a name="7004-POSI-013" href="#7004-POSI-013">7004-POSI-013</a>)
- **Must** be able to see worst case liquidation price (<a name="7004-POSI-016" href="#7004-POSI-016">7004-POSI-016</a>)
- **Must** be able to see best and worst case liquidation price when hovering (<a name="7004-POSI-017" href="#7004-POSI-017">7004-POSI-017</a>)
- **Must** be able to see if your realised PnL was affected by loss socialisation (<a name="7004-POSI-018" href="#7004-POSI-018">7004-POSI-018</a>)
- **Must** be able to see what type of product the position was opened on (<a name="7004-POSI-019" href="#7004-POSI-019">7004-POSI-019</a>)
- **Must** not see positions on markets which are closed (<a name="7004-POSI-020" href="#7004-POSI-020">7004-POSI-020</a>)
- **Must** be able to show closed markets (<a name="7004-POSI-021" href="#7004-POSI-021">7004-POSI-021</a>)