From cae1fc67a4a0f7a4405487e1b03b9d5a7a9172a1 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Wed, 16 Aug 2023 22:58:14 +0100 Subject: [PATCH 01/15] fix(governance): validators display fixes (#4547) --- .../governance/src/i18n/translations/dev.json | 4 +- .../consensus-validators-table.tsx | 4 +- .../standby-pending-validators-table.tsx | 13 +++++- .../validator-tables/validator-tables.tsx | 46 ++++++++++++------- apps/governance/src/routes/staking/shared.ts | 2 +- .../src/use-network-params.ts | 1 + 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index 62f3a1911..f8b65b066 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -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", diff --git a/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx b/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx index ef55dca05..cb25126fc 100644 --- a/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx +++ b/apps/governance/src/routes/staking/home/validator-tables/consensus-validators-table.tsx @@ -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, diff --git a/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx b/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx index 9f12b7eda..c3a1d55ef 100644 --- a/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx +++ b/apps/governance/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx @@ -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, diff --git a/apps/governance/src/routes/staking/home/validator-tables/validator-tables.tsx b/apps/governance/src/routes/staking/home/validator-tables/validator-tables.tsx index d4802d14d..01071f64d 100644 --- a/apps/governance/src/routes/staking/home/validator-tables/validator-tables.tsx +++ b/apps/governance/src/routes/staking/home/validator-tables/validator-tables.tsx @@ -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('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 (
@@ -187,12 +193,18 @@ export const ValidatorTables = ({

- + + + + + + {' '} + {stakeNeededForPromotion}{' '} + + + + +

{ } 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; diff --git a/libs/network-parameters/src/use-network-params.ts b/libs/network-parameters/src/use-network-params.ts index bdb68e663..c0b7afc9c 100644 --- a/libs/network-parameters/src/use-network-params.ts +++ b/libs/network-parameters/src/use-network-params.ts @@ -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; From 140f16f6372b062a9ae25ada1b82552589efb282 Mon Sep 17 00:00:00 2001 From: daro-maj <119658839+daro-maj@users.noreply.github.com> Date: Thu, 17 Aug 2023 11:10:28 +0300 Subject: [PATCH 02/15] test(trading): adjust tests edit and cancel orders (#4563) --- apps/trading-e2e/src/integration/trading-orders.cy.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/trading-e2e/src/integration/trading-orders.cy.ts b/apps/trading-e2e/src/integration/trading-orders.cy.ts index e6396ae4e..31ceb2818 100644 --- a/apps/trading-e2e/src/integration/trading-orders.cy.ts +++ b/apps/trading-e2e/src/integration/trading-orders.cy.ts @@ -447,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'); @@ -476,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 = { @@ -514,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'); From f3b72b894e52a5c19f832b61400c26ed28576642 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Thu, 17 Aug 2023 10:27:47 +0100 Subject: [PATCH 03/15] feat(explorer): view 'Cancel Stop Order' Transactions (#4521) --- .../txs/details/tx-details-wrapper.tsx | 3 + .../txs/details/tx-stop-order-cancel.tsx | 60 +++++++++++++++++++ .../src/app/components/txs/tx-filter.tsx | 2 + 3 files changed, 65 insertions(+) create mode 100644 apps/explorer/src/app/components/txs/details/tx-stop-order-cancel.tsx diff --git a/apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx b/apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx index 70411d635..509deb263 100644 --- a/apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx @@ -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': diff --git a/apps/explorer/src/app/components/txs/details/tx-stop-order-cancel.tsx b/apps/explorer/src/app/components/txs/details/tx-stop-order-cancel.tsx new file mode 100644 index 000000000..629d5fb2d --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-stop-order-cancel.tsx @@ -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 ( + + + + {t('Cancel stop order')} + + {stopOrderId ? ( + + ) : ( + + )} + + + {marketId ? ( + + {t('Market')} + + + + + ) : null} + + ); +}; diff --git a/apps/explorer/src/app/components/txs/tx-filter.tsx b/apps/explorer/src/app/components/txs/tx-filter.tsx index b2c7b4446..b0add2253 100644 --- a/apps/explorer/src/app/components/txs/tx-filter.tsx +++ b/apps/explorer/src/app/components/txs/tx-filter.tsx @@ -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', From 611f8d74917b4d2c2e1fefa2b2bf27459309ca27 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 17 Aug 2023 11:07:48 +0100 Subject: [PATCH 04/15] feat(positions): table layout changes (#4558) --- .../src/integration/trading-positions.cy.ts | 372 ----------- .../market-selector/market-selector-item.tsx | 10 +- .../src/lib/cells/market-name-cell.tsx | 33 +- libs/positions/src/lib/liquidation-price.tsx | 83 +-- .../src/lib/positions-data-providers.spec.ts | 8 +- .../src/lib/positions-data-providers.ts | 38 +- libs/positions/src/lib/positions-manager.tsx | 2 +- .../src/lib/positions-table.spec.tsx | 360 +++++------ libs/positions/src/lib/positions-table.tsx | 612 +++++++++--------- libs/positions/src/lib/stacked-cell.tsx | 18 + .../proposals-list/use-column-defs.tsx | 25 +- .../src/utils/get-market-product-type.spec.ts | 58 -- .../src/utils/get-market-product-type.ts | 16 - libs/types/src/global-types-mappings.ts | 18 + libs/types/src/index.ts | 1 + libs/types/src/product.ts | 7 + .../src/components/tooltip/tooltip.tsx | 6 +- libs/utils/src/lib/format/number.spec.ts | 2 + 18 files changed, 635 insertions(+), 1034 deletions(-) delete mode 100644 apps/trading-e2e/src/integration/trading-positions.cy.ts create mode 100644 libs/positions/src/lib/stacked-cell.tsx delete mode 100644 libs/proposals/src/utils/get-market-product-type.spec.ts delete mode 100644 libs/proposals/src/utils/get-market-product-type.ts create mode 100644 libs/types/src/product.ts diff --git a/apps/trading-e2e/src/integration/trading-positions.cy.ts b/apps/trading-e2e/src/integration/trading-positions.cy.ts deleted file mode 100644 index 3ab6b46d3..000000000 --- a/apps/trading-e2e/src/integration/trading-positions.cy.ts +++ /dev/null @@ -1,372 +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, - ' [data-testid="market-code"]' - ); - }); - - // 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(); -} diff --git a/apps/trading/components/market-selector/market-selector-item.tsx b/apps/trading/components/market-selector/market-selector-item.tsx index 8d5479024..b60ef54af 100644 --- a/apps/trading/components/market-selector/market-selector-item.tsx +++ b/apps/trading/components/market-selector/market-selector-item.tsx @@ -89,17 +89,15 @@ const MarketData = ({ ? addDecimalsFormatNumber(vol, market.positionDecimalPlaces) : '0.00'; + const productType = market.tradableInstrument.instrument.product.__typename; + return ( <>

{market.tradableInstrument.instrument.code}{' '} - {allProducts && ( - + {allProducts && productType && ( + )}

{mode && ( diff --git a/libs/datagrid/src/lib/cells/market-name-cell.tsx b/libs/datagrid/src/lib/cells/market-name-cell.tsx index 3ccc37167..d1df34e5d 100644 --- a/libs/datagrid/src/lib/cells/market-name-cell.tsx +++ b/libs/datagrid/src/lib/cells/market-name-cell.tsx @@ -2,28 +2,27 @@ import type { MouseEvent } from 'react'; import { useCallback } from 'react'; import get from 'lodash/get'; import { Pill } from '@vegaprotocol/ui-toolkit'; -import type { Market } from '@vegaprotocol/types'; - -const productTypeMap = { - Future: 'Futr', - FutureProduct: 'Futr', - Spot: 'Spot', - SpotProduct: 'Spot', - Perpetual: 'Perp', - PerpetualProduct: 'Perp', -} as const; -export type ProductType = keyof typeof productTypeMap | undefined; +import { + ProductTypeShortName, + type Market, + type ProductType, + ProductTypeMapping, +} from '@vegaprotocol/types'; export const MarketProductPill = ({ productType, }: { - productType?: ProductType; + productType: ProductType; }) => { - return productType ? ( - - {productTypeMap[productType] || productType} + return ( + + {ProductTypeShortName[productType]} - ) : null; + ); }; interface MarketNameCellProps { @@ -66,7 +65,7 @@ export const MarketNameCell = ({ {value} - + {productType && } ); return onMarketClick && id ? ( diff --git a/libs/positions/src/lib/liquidation-price.tsx b/libs/positions/src/lib/liquidation-price.tsx index ae08e806b..f5afd0ccb 100644 --- a/libs/positions/src/lib/liquidation-price.tsx +++ b/libs/positions/src/lib/liquidation-price.tsx @@ -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 -; } - return {value}; + + 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 ( + + + + {t('Worst case')} + {worstCase} + + + {t('Best case')} + {bestCase} + + + + } + > + {worstCase} + + ); }; diff --git a/libs/positions/src/lib/positions-data-providers.spec.ts b/libs/positions/src/lib/positions-data-providers.spec.ts index bb7180d11..a8ca0aaa9 100644 --- a/libs/positions/src/lib/positions-data-providers.spec.ts +++ b/libs/positions/src/lib/positions-data-providers.spec.ts @@ -180,12 +180,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 +205,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'); diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts index 572a551fd..8d44c0941 100644 --- a/libs/positions/src/lib/positions-data-providers.ts +++ b/libs/positions/src/lib/positions-data-providers.ts @@ -26,20 +26,20 @@ 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; markPrice: string | undefined; notional: string | undefined; @@ -51,7 +51,7 @@ export interface Position { totalBalance: string; unrealisedPNL: string; updatedAt: string | null; - productType?: string; + productType: ProductType; } export const getMetrics = ( @@ -71,15 +71,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 ); @@ -89,11 +84,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 @@ -112,17 +107,17 @@ 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, markPrice: marketData ? marketData.markPrice : undefined, notional: notional @@ -133,10 +128,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, + productType: market?.tradableInstrument.instrument.product + .__typename as ProductType, }); }); return metrics; @@ -288,7 +284,7 @@ export const positionsMetricsProvider = makeDerivedDataProvider< ([positions, accounts, marketsData]) => { const positionsData = rejoinPositionData(positions, marketsData); const metrics = getMetrics(positionsData, accounts as Account[] | null); - return sortBy(metrics, 'marketName'); + return sortBy(metrics, 'marketCode'); }, (data, delta, previousData) => data.filter((row) => { diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index 45c17f173..f01f71cfa 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -15,7 +15,7 @@ interface PositionsManagerProps { partyIds: string[]; onMarketClick?: (marketId: string) => void; isReadOnly: boolean; - gridProps: ReturnType; + gridProps?: ReturnType; } export const PositionsManager = ({ diff --git a/libs/positions/src/lib/positions-table.spec.tsx b/libs/positions/src/lib/positions-table.spec.tsx index 4665cd002..be4532bfe 100644 --- a/libs/positions/src/lib/positions-table.spec.tsx +++ b/libs/positions/src/lib/positions-table.spec.tsx @@ -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,13 +19,13 @@ 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, markPrice: '123', notional: '12300', @@ -40,9 +39,13 @@ const singleRow: Position = { productType: 'Future', }; -const singleRowData = [singleRow]; - describe('Positions', () => { + const renderComponent = async (rowData: Position) => { + await act(async () => { + render(); + }); + }; + it('should render successfully', async () => { await act(async () => { const { baseElement } = render( @@ -53,158 +56,132 @@ describe('Positions', () => { }); it('render correct columns', async () => { - await act(async () => { - render(); - }); - - 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(); - }); - 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(); - }); - + 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( - - ); - }); - 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( - - ); - }); - 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( - - ); + 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( - - ); - }); - - 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(); - }); + await renderComponent(singleRow); const cells = screen.getAllByRole('gridcell'); expect(cells[4].textContent).toEqual('liquidation price'); }); - it('displays leverage', async () => { - await act(async () => { - render(); - }); + 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(); - }); - 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(); - }); + 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( { return; }} @@ -212,24 +189,15 @@ describe('Positions', () => { /> ); }); - const cells = screen.getAllByRole('gridcell'); - expect(cells[11].textContent).toEqual(''); + + expect(screen.getByTestId('close-position')).toBeInTheDocument(); }); it('do not display close button if openVolume is zero', async () => { - await act(async () => { - render( - { - 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', () => { @@ -249,40 +217,27 @@ describe('Positions', () => { lossSocialisationAmount: '0', }, valueFormatted: '100', - }; - render(); - expect(screen.getByText(props.valueFormatted)).toBeInTheDocument(); - expect(screen.queryByRole('img')).not.toBeInTheDocument(); + } as ICellRendererParams; + render(); + 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(); 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(); }); }); @@ -290,9 +245,10 @@ describe('Positions', () => { const props = { data: undefined, valueFormatted: '100', - }; + } as ICellRendererParams; + it('renders a dash if no data', () => { - render(); + render(); expect(screen.getByText('-')).toBeInTheDocument(); }); @@ -306,36 +262,21 @@ describe('Positions', () => { }; render(); 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(); - const content = screen.getByText(props.valueFormatted); + } as ICellRendererParams; + render(); + 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 () => { @@ -345,24 +286,71 @@ describe('Positions', () => { status: PositionStatus.POSITION_STATUS_CLOSED_OUT, }, valueFormatted: '100', - }; - render(); - const content = screen.getByText(props.valueFormatted); + } as ICellRendererParams; + render(); + 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(); }); diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx index 8aebf7458..66e0ea9bf 100644 --- a/libs/positions/src/lib/positions-table.tsx +++ b/libs/positions/src/lib/positions-table.tsx @@ -1,46 +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, - VegaIconNames, 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 { onClose?: (data: Position) => void; @@ -48,44 +48,25 @@ interface Props extends TypedDataAgGrid { 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 ? ( -
-
- {volumePrefix( - addDecimalsFormatNumber(openVolume, positionDecimalPlaces) - )} -
-
- {addDecimalsFormatNumber(notional, marketDecimalPlaces)} -
-
- ) : 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, @@ -103,7 +84,6 @@ export const PositionsTable = ({ pubKey, ...props }: Props) => { - const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); return ( (() => { const columnDefs: (ColDef | null)[] = [ multipleKeys @@ -132,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) => { - 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) => { - return !data || !data.notional - ? '-' - : addDecimalsFormatNumber( - data.notional, - data.marketDecimalPlaces - ); + }: VegaICellRendererParams) => { + if (!data || !value) return '-'; + return ( + + {data?.assetSymbol} + + + } + /> + ); }, }, { - headerName: t('Open volume'), + headerName: t('Size / Notional'), field: 'openVolume', type: 'rightAligned', cellClass: 'font-mono text-right', cellClassRules: signedNumberCssClassRules, filter: 'agNumberColumnFilter', - valueGetter: ({ data }: VegaValueGetterParams) => { + valueGetter: ({ data }: { data: Position }) => { return data?.openVolume === undefined ? undefined : toBigNum( @@ -174,165 +150,205 @@ export const PositionsTable = ({ data.positionDecimalPlaces ).toNumber(); }, + tooltipValueGetter: ({ data }: ITooltipParams) => { + if ( + !data || + data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED + ) { + return null; + } + return data.status; + }, valueFormatter: ({ data, }: VegaValueFormatterParams): 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) => { + 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 ( + +

{primaryTooltip}

+

{secondaryTooltip}

+

+ {t( + 'Status: %s', + PositionStatusMapping[args.data.status] + )} +

+ {POSITION_RESOLUTION_LINK && ( + + {t('Read more about position resolution')} + + )} + + } + /> + ); }, cellRenderer: OpenVolumeCell, }, { - headerName: t('Mark price'), + headerName: t('Entry / Mark'), field: 'markPrice', type: 'rightAligned', - cellRenderer: PriceFlashCell, + cellClass: 'font-mono text-right', + cellRenderer: ({ + data, + }: VegaICellRendererParams) => { + 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 ( + + } + /> + ); + }, filter: 'agNumberColumnFilter', valueGetter: ({ data }: VegaValueGetterParams) => { 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) => { - if (!data) { - return ''; - } + }, + { + headerName: t('Margin'), + colId: 'margin', + type: 'rightAligned', + cellClass: 'font-mono text-right', + filter: 'agNumberColumnFilter', + valueGetter: ({ data }: VegaValueGetterParams) => { + return !data + ? undefined + : toBigNum( + data.marginAccountBalance, + data.assetDecimals + ).toNumber(); + }, + cellRenderer: ({ data }: VegaICellRendererParams) => { 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 ( + ); }, }, { - 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) => { - if (!data) return null; + if (!data) { + return '-'; + } return ( ); }, }, - { - headerName: t('Asset'), - field: 'assetSymbol', - colId: 'asset', - cellRenderer: ({ data }: VegaICellRendererParams) => { - if (!data) return null; - return ( - { - openAssetDetailsDialog( - data.assetId, - e.target as HTMLElement - ); - }} - > - {data?.assetSymbol} - - ); - }, - }, - { - headerName: t('Entry price'), - field: 'averageEntryPrice', - type: 'rightAligned', - cellRenderer: PriceFlashCell, - filter: 'agNumberColumnFilter', - valueGetter: ({ data }: VegaValueGetterParams) => { - 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) => - value === undefined ? '' : formatNumber(value.toString(), 1), - }, - multipleKeys - ? null - : { - headerName: t('Margin'), - field: 'marginAccountBalance', - type: 'rightAligned', - filter: 'agNumberColumnFilter', - cellRenderer: PriceFlashCell, - valueGetter: ({ data }: VegaValueGetterParams) => { - 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', @@ -340,17 +356,71 @@ export const PositionsTable = ({ cellClassRules: signedNumberCssClassRules, cellClass: 'font-mono text-right', filter: 'agNumberColumnFilter', - valueGetter: ({ data }: VegaValueGetterParams) => { - 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 ( + +

+ {t('Realised PNL: %s', args.value)} +

+

+ {t( + 'Lifetime loss socialisation deductions: %s', + lossesFormatted + )} +

+

+ {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 + )} +

+ {LOSS_SOCIALIZATION_LINK && ( + + {t('Read more about loss socialisation')} + + )} + + } + /> + ); }, valueFormatter: ({ data, }: VegaValueFormatterParams) => { 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.' @@ -364,21 +434,22 @@ export const PositionsTable = ({ cellClassRules: signedNumberCssClassRules, cellClass: 'font-mono text-right', filter: 'agNumberColumnFilter', - valueGetter: ({ data }: VegaValueGetterParams) => { - 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) => !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 ? { @@ -410,28 +481,16 @@ export const PositionsTable = ({ return columnDefs.filter( (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) => { - const LOSS_SOCIALIZATION_LINK = DocsLinks?.LOSS_SOCIALIZATION ?? ''; - if (!data) { return <>-; } @@ -442,121 +501,62 @@ export const PNLCell = ({ return <>{valueFormatted}; } - const lossesFormatted = addDecimalsFormatNumber( - data.lossSocializationAmount, - data.decimals - ); - - return ( - -

- {t('Lifetime loss socialisation deductions: %s', lossesFormatted)} -

-

- {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] - )} -

- {LOSS_SOCIALIZATION_LINK && ( - - {t('Read more about loss socialisation')} - - )} - - } - > - {valueFormatted} -
- ); + return {valueFormatted}; }; export const OpenVolumeCell = ({ valueFormatted, data, }: VegaICellRendererParams) => { - 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 = ( + + ); + + 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 ( -

{primaryTooltip}

-

{secondaryTooltip}

-

- {t('Status: %s', PositionStatusMapping[data.status])} -

- {POSITION_RESOLUTION_LINK && ( - - {t('Read more about position resolution')} - - )} - + 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}
); }; const WarningCell = ({ children, - tooltipContent, showIcon = true, }: { children: ReactNode; - tooltipContent: ReactNode; showIcon?: boolean; }) => { return ( - -
- - {showIcon && } +
+ {showIcon && ( + + - {children} -
- + )} + + {children} + +
); }; diff --git a/libs/positions/src/lib/stacked-cell.tsx b/libs/positions/src/lib/stacked-cell.tsx new file mode 100644 index 000000000..0f5ab5c8a --- /dev/null +++ b/libs/positions/src/lib/stacked-cell.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from 'react'; + +export const StackedCell = ({ + primary, + secondary, +}: { + primary: ReactNode; + secondary: ReactNode; +}) => { + return ( +
+
{primary}
+
+ {secondary} +
+
+ ); +}; diff --git a/libs/proposals/src/components/proposals-list/use-column-defs.tsx b/libs/proposals/src/components/proposals-list/use-column-defs.tsx index 6bb0a1d7a..795a0fa8c 100644 --- a/libs/proposals/src/components/proposals-list/use-column-defs.tsx +++ b/libs/proposals/src/components/proposals-list/use-column-defs.tsx @@ -5,7 +5,6 @@ import { CenteredGridCellWrapper, COL_DEFS, DateRangeFilter, - MarketProductPill, SetFilter, } from '@vegaprotocol/datagrid'; import compact from 'lodash/compact'; @@ -20,13 +19,15 @@ import type { VegaICellRendererParams, VegaValueFormatterParams, } from '@vegaprotocol/datagrid'; -import { ExternalLink } from '@vegaprotocol/ui-toolkit'; -import type { InstrumentConfiguration } from '@vegaprotocol/types'; -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'; -import { getMarketProductType } from '../../utils/get-market-product-type'; export const MarketNameProposalCell = ({ value, @@ -38,13 +39,19 @@ export const MarketNameProposalCell = ({ const { VEGA_TOKEN_URL } = useEnvironment(); const { change } = data?.terms || {}; if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) { - const productType = getMarketProductType( - change.instrument as InstrumentConfiguration - ); + const type = change.instrument.futureProduct?.__typename; const content = ( <> {value as string} - + {type && ( + + {ProposalProductTypeShortName[type]} + + )} ); if (data?.id) { diff --git a/libs/proposals/src/utils/get-market-product-type.spec.ts b/libs/proposals/src/utils/get-market-product-type.spec.ts deleted file mode 100644 index b7696a1cf..000000000 --- a/libs/proposals/src/utils/get-market-product-type.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { InstrumentConfiguration } from '@vegaprotocol/types'; -import { getMarketProductType } from './get-market-product-type'; - -describe('getMarketProductType', () => { - it('should resolve product type properly', () => { - expect( - getMarketProductType({ - futureProduct: { - quoteName: 'Market 1', - }, - } as InstrumentConfiguration) - ).toEqual('Future'); - expect( - getMarketProductType({ - spotProduct: { - quoteName: 'Market 1', - }, - } as unknown as InstrumentConfiguration) - ).toEqual('Spot'); - expect( - getMarketProductType({ - perpetualProduct: { - quoteName: 'Market 1', - }, - } as unknown as InstrumentConfiguration) - ).toEqual('Perpetual'); - expect( - getMarketProductType({ - product: { - __typename: 'Perpetual', - }, - futureProduct: { - quoteName: 'Market 1', - }, - } as unknown as InstrumentConfiguration) - ).toEqual('Perpetual'); - expect( - getMarketProductType({ - product: { - __typename: 'Spot', - }, - futureProduct: { - quoteName: 'Market 1', - }, - } as unknown as InstrumentConfiguration) - ).toEqual('Spot'); - expect( - getMarketProductType({ - product: { - __typename: 'Future', - }, - perpetualProduct: { - quoteName: 'Market 1', - }, - } as unknown as InstrumentConfiguration) - ).toEqual('Future'); - }); -}); diff --git a/libs/proposals/src/utils/get-market-product-type.ts b/libs/proposals/src/utils/get-market-product-type.ts deleted file mode 100644 index 58be38db8..000000000 --- a/libs/proposals/src/utils/get-market-product-type.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { InstrumentConfiguration, Product } from '@vegaprotocol/types'; - -// it needs to be adjusted after deploy this https://github.com/vegaprotocol/vega/pull/9003 -export const getMarketProductType = ( - instrumentConfiguration: InstrumentConfiguration -) => { - return 'product' in instrumentConfiguration - ? (instrumentConfiguration.product as Product).__typename - : 'futureProduct' in instrumentConfiguration - ? 'Future' - : 'spotProduct' in instrumentConfiguration - ? 'Spot' - : 'perpetualProduct' in instrumentConfiguration - ? 'Perpetual' - : undefined; -}; diff --git a/libs/types/src/global-types-mappings.ts b/libs/types/src/global-types-mappings.ts index 4458530e2..8912ff379 100644 --- a/libs/types/src/global-types-mappings.ts +++ b/libs/types/src/global-types-mappings.ts @@ -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 = { + Future: 'Future', +}; + +export const ProductTypeShortName: Record = { + Future: 'Futr', +}; + +export const ProposalProductTypeMapping: Record = { + FutureProduct: 'Future', +}; + +export const ProposalProductTypeShortName: Record = + { + FutureProduct: 'Futr', + }; diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 763376bbb..620b07f56 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -1,3 +1,4 @@ export * from './__generated__/types'; export * from './candle'; export * from './global-types-mappings'; +export * from './product'; diff --git a/libs/types/src/product.ts b/libs/types/src/product.ts new file mode 100644 index 000000000..c395539d9 --- /dev/null +++ b/libs/types/src/product.ts @@ -0,0 +1,7 @@ +import type { Product } from './__generated__/types'; + +export type ProductType = NonNullable; + +// TODO: Update to be dynamically created for ProductionConfiguration union when schema +// changes make it to stagnet1 +export type ProposalProductType = 'FutureProduct'; diff --git a/libs/ui-toolkit/src/components/tooltip/tooltip.tsx b/libs/ui-toolkit/src/components/tooltip/tooltip.tsx index ba65ecfd1..9c629d785 100644 --- a/libs/ui-toolkit/src/components/tooltip/tooltip.tsx +++ b/libs/ui-toolkit/src/components/tooltip/tooltip.tsx @@ -60,5 +60,9 @@ export const Tooltip = ({ ); export const TooltipCellComponent = (props: ITooltipParams) => { - return

{props.value}

; + return ( +
+ {props.value} +
+ ); }; diff --git a/libs/utils/src/lib/format/number.spec.ts b/libs/utils/src/lib/format/number.spec.ts index 0dadcaa76..8ff305247 100644 --- a/libs/utils/src/lib/format/number.spec.ts +++ b/libs/utils/src/lib/format/number.spec.ts @@ -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 }) => { From 7142fc956ce67546850cbfc4d1087ebf50da41bf Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 17 Aug 2023 13:07:14 +0100 Subject: [PATCH 05/15] chore(positions): spec updates for positions table changes (#4568) --- specs/7004-POSI-positions.md | 41 ++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/specs/7004-POSI-positions.md b/specs/7004-POSI-positions.md index d3ee1ca3b..3bcb69493 100644 --- a/specs/7004-POSI-positions.md +++ b/specs/7004-POSI-positions.md @@ -4,30 +4,37 @@ - **Must** be able to view the following columns (7004-POSI-002): - - 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 (7004-POSI-003) + - If the cell shows two values the primary (top value) is used to sort (7004-POSI-014) + - **Must** be able to scroll horizontally to see all columns if screen resolution isn't wide enough (7004-POSI-004) - **Must** be able to drag and drop column names to re order (7004-POSI-005) +- **Must** be able to remove columns by dragging headers out of the table (7004-POSI-015) + - **Must** be able to resize the width of the columns (7004-POSI-006) - **Must** The columns 'Open volume', 'realised PNL' and 'Unrealised PNL' must change color and have a + or - suffix depending on being positive/negative (7004-POSI-007) -- **Must** be able to select settlement asset to view asset details (7004-POSI-008) - - **Must** be able to select the 3 dots to view asset details (7004-POSI-009) - **Must** be able to select the close button to close my position (7004-POSI-010) @@ -37,3 +44,11 @@ - **Must** retain previous column sizing on refresh (7004-POSI-012) - **Must** notional column has a tooltip on hover (7004-POSI-013) + +- **Must** be able to see worst case liquidation price (7004-POSI-016) + +- **Must** be able to see best and worst case liquidation price when hovering (7004-POSI-017) + +- **Must** be able to see if your realised PnL was affected by loss socialisation (7004-POSI-018) + +- **Must** Must be able to see what type of product the position was opened on (7004-POSI-019) From 7471116ebfad9ea694b71650fe3fa4124a8a47c5 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 17 Aug 2023 17:08:27 +0100 Subject: [PATCH 06/15] chore(trading): remove market selector test (#4572) --- .../src/integration/market-selector.cy.ts | 136 ------------------ 1 file changed, 136 deletions(-) delete mode 100644 apps/trading-e2e/src/integration/market-selector.cy.ts diff --git a/apps/trading-e2e/src/integration/market-selector.cy.ts b/apps/trading-e2e/src/integration/market-selector.cy.ts deleted file mode 100644 index 5bd93e767..000000000 --- a/apps/trading-e2e/src/integration/market-selector.cy.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - AuctionTrigger, - MarketState, - MarketTradingMode, -} from '@vegaprotocol/types'; - -describe('markets selector', { tags: '@smoke' }, () => { - const list = 'market-selector-list'; - const searchInput = 'search-term'; - - beforeEach(() => { - cy.window().then((window) => { - window.localStorage.setItem('marketId', 'market-1'); - }); - cy.setOnBoardingViewed(); - cy.mockTradingPage( - MarketState.STATE_ACTIVE, - MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, - AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET - ); - cy.mockSubscription(); - cy.visit('/'); - - cy.wait('@Markets'); - cy.wait('@MarketsData'); - }); - - // 6001-MARK-066 - it('can open popover to view markets', () => { - cy.getByTestId('market-selector').should('not.exist'); - cy.getByTestId('header-title').should('be.visible').click(); - cy.getByTestId('market-selector').should('be.visible'); - }); - - // need function keyword as we need 'this' to access market data - it('displays data as expected', () => { - // TODO: load data from mocks in. Using alias and wrap intermittently fails - const data = [ - { - 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(); - cy.getByTestId(list) - .find('a') - .each((item, i) => { - const market = data[i]; - // 6001-MARK-021 - // 6001-MARK-022 - expect(item.find('h3').text()).equals( - `${market.code} ${market.productType}` - ); - expect( - item.find('[data-testid="market-selector-volume"]').text() - ).contains(market.vol); - // 6001-MARK-024 - expect( - item.find('[data-testid="market-selector-price"]').text() - ).contains(market.markPrice); - // 6001-MARK-025 - expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist; - }); - }); - - it('can use the filter options', () => { - cy.getByTestId('header-title').should('be.visible').click(); - - // 6001-MARK-027 - // product type - cy.getByTestId('product-Spot').click(); - cy.getByTestId(list).contains('Spot markets coming soon.'); - cy.getByTestId('product-Perpetual').click(); - cy.getByTestId(list).contains('Perpetual markets coming soon.'); - cy.getByTestId('product-Future').click(); - cy.getByTestId(list).find('a').should('have.length', 4); - - // 6001-MARK-029 - cy.getByTestId(searchInput).clear().type('btc'); - cy.getByTestId(list).find('a').should('have.length', 2); - cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21'); - cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21'); - - cy.getByTestId(searchInput).clear(); - cy.getByTestId(list).find('a').should('have.length', 4); - }); - - it('can sort by by top gaining and top losing market', () => { - cy.getByTestId('header-title').should('be.visible').click(); - - // 6001-MARK-030 - // 6001-MARK-031 - // 6001-MARK-032 - // 6001-MARK-033 - cy.getByTestId(' sort-trigger').click(); - cy.getByTestId('sort-item-Gained') - .contains('Top gaining') - .should('be.visible'); - cy.getByTestId('sort-item-Lost') - .contains('Top losing') - .should('be.visible'); - cy.getByTestId('sort-item-New') - .contains('New markets') - .should('be.visible'); - }); - - it('can filter by settlement asset', () => { - cy.getByTestId('header-title').should('be.visible').click(); - - // 6001-MARK-028 - cy.getByTestId('asset-trigger').click(); - cy.getByTestId('asset-id-asset-3').contains('tBTC').click(); - cy.getByTestId(list).find('a').should('have.length', 1); - cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21'); - }); -}); From ef26d03d363c213415085465707f1dca331a67f8 Mon Sep 17 00:00:00 2001 From: Maciek Date: Thu, 17 Aug 2023 18:27:15 +0200 Subject: [PATCH 07/15] chore(markets): distinguish between product types - add specs (#4564) Co-authored-by: Matthew Russell --- specs/6001-MARK-find_markets.md | 4 ++++ specs/7001-COLL-collateral.md | 35 +++++++++++++++++++++++--------- specs/7003-MORD-manage_orders.md | 1 + specs/9001-DATA-data_display.md | 4 ++++ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/specs/6001-MARK-find_markets.md b/specs/6001-MARK-find_markets.md index 439154ac1..71f4d5c83 100644 --- a/specs/6001-MARK-find_markets.md +++ b/specs/6001-MARK-find_markets.md @@ -3,6 +3,7 @@ ## Closed Markets - **Must** see market's instrument code (6001-MARK-001) +- **Must** see market's product type (6001-MARK-071) - **Must** see market's instrument name (sometimes labelled 'description') (6001-MARK-002) - **Must** see status (6001-MARK-003) - **Must** see the settlement date (6001-MARK-004) @@ -45,10 +46,12 @@ - **Must** be able to close and open the market selector (6001-MARK-066) - **Must** must change color and have + or negative suffix of the price change and change color for the sparkline (6001-MARK-067) - **Must** be default tab "All" where there's no filtering by product. (6001-MARK-070) +- If tab "All" is selected **Must** see product type (6001-MARK-072) ## All Markets - **Must** see market's instrument code (6001-MARK-035) +- **Must** see product type (6001-MARK-073) - **Must** see market's instrument name (sometimes labelled 'description') (6001-MARK-036) - **Must** see Trading mode (6001-MARK-037) - **Must** see status (6001-MARK-038) @@ -71,6 +74,7 @@ ## Proposed markets - **Must** see market's instrument code (6001-MARK-049) +- **Must** see product type (6001-MARK-074) - **Must** see market's instrument name (sometimes labelled 'description') (6001-MARK-050) - **Must** show the settlement asset (6001-MARK-051) - **Must** see state (6001-MARK-052) diff --git a/specs/7001-COLL-collateral.md b/specs/7001-COLL-collateral.md index 59d15ea12..ccfc71673 100644 --- a/specs/7001-COLL-collateral.md +++ b/specs/7001-COLL-collateral.md @@ -13,16 +13,31 @@ - **Must** show the asset symbol (7001-COLL-007) - **Must** provide a way to see the [full asset details](6501-ASSE-assets.md) (7001-COLL-008) - **Must** provide a way to see all accounts, their type, and their balance for a single asset (7001-COLL-009) - - **Could** have default sort order (7001-COLL-010) - - General - - Margin - - Bond - - Fees - Maker - - Fees - Liquidity - - Rewards - Maker Paid - - Rewards - Maker Received - - Rewards - Liquidity Provision Received Fees - - Rewards - Market Proposers + - **Could** have default sort order (7001-COLL-010) + - General + - Margin + - Bond + - Fees - Maker + - Fees - Liquidity + - Rewards - Maker Paid + - Rewards - Maker Received + - Rewards - Liquidity Provision Received Fees + - Rewards - Market Proposers + +## Accounts breakdown + +- **Must** be able to see a breakdown of accounts for a single asset (7001-COLL-013) +- **Must** be able to see the total amount of the selected asset (7001-COLL-014) +- **Must** be able to see market code of margin account (7001-COLL-015) +- **Must** be able to see product type of the margin account's associated market (7001-COLL-016) +- **Must** be able to see account type (7001-COLL-017) +- **Must** be able to see account balance (7001-COLL-018) +- **Must** be able to see what percentage of that assets total is used by a margin account (7001-COLL-019) +- **Must** be able to see margin health if its a margin account (7001-COLL-020) + +### Margin health + +TODO ## Deal Ticket diff --git a/specs/7003-MORD-manage_orders.md b/specs/7003-MORD-manage_orders.md index 81c62a875..d9ffa0a56 100644 --- a/specs/7003-MORD-manage_orders.md +++ b/specs/7003-MORD-manage_orders.md @@ -51,6 +51,7 @@ When looking at a list of orders, I... - **must** see what [market](9001-DATA-data_display.md#market) an order is related to (either code, ID or name, preferable name) (7003-MORD-002) - **should** see what the `status` is of the market (particularly if it is not "normal") +- **must** see product type of market's instrument (7003-MORD-020) - **must** see the [size](9001-DATA-data_display.md#size) of the order (7003-MORD-003) - **must** see the [direction/side](9001-DATA-data_display.md#direction--side) (Long or Short) of the order (this can be implied with a + or negative suffix on the size, + for Long, - for short) (7003-MORD-004) - **must** see [order type](9001-DATA-data_display.md#order-type) (7003-MORD-005) diff --git a/specs/9001-DATA-data_display.md b/specs/9001-DATA-data_display.md index 4febec14a..f6ec7699d 100644 --- a/specs/9001-DATA-data_display.md +++ b/specs/9001-DATA-data_display.md @@ -57,9 +57,13 @@ The quantum is a value that is used to define "The minimum economically meaningf ## Market Markets do not have names, technically it is the instrument within a market that has the name. Theoretically the same instrument can be traded in multiple markets. if/when this happens a user needs to be able to disambiguate between markets. Each market does have a unique ID, Note: this is a hash of the definition of the market when it was created. + Instruments have both a Name and Code, see [market framework](../protocol/0001-MKTF-market_framework.md) for how these are used. Generally the Code can save space once a user is familiar with the market. The Name is more descriptive and should be the default when discovering markets. It remains to be seen how the community will use these exactly. + Markets can have several statuses and it may be sensible when listing markets to highlight their status. e.g. if a market is usually in continuous trading mode, but is currently in an auction due to low liquidity. The market name field could be augmented to show the status (add an icon etc). +Near to instrument code or name its product type should be shown using the short name (Futr, Spot, Perp) or if space allows for it, then the long name may be used (Future, Spot, Perpetual). + ## Public keys > aka Party From afbf62be84570580889d67ce9ffabf0f3b949b81 Mon Sep 17 00:00:00 2001 From: Joe Tsang <30622993+jtsang586@users.noreply.github.com> Date: Fri, 18 Aug 2023 16:15:55 +0100 Subject: [PATCH 08/15] chore(governance): add assertion for vote change (#4571) --- .../src/integration/flow/proposal-details.cy.ts | 1 + apps/governance-e2e/src/support/staking.functions.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts b/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts index a8defbc08..cef462d61 100644 --- a/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts +++ b/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts @@ -220,6 +220,7 @@ describe( cy.getByTestId(changeVoteButton).should('be.visible').click(); voteForProposal('for'); // 3001-VOTE-064 + cy.getByTestId('user-voted-yes').should('exist'); getProposalInformationFromTable('Tokens for proposal') .should('have.text', (1).toFixed(2)) .and('be.visible'); diff --git a/apps/governance-e2e/src/support/staking.functions.ts b/apps/governance-e2e/src/support/staking.functions.ts index db3a3cdef..4f99e0964 100644 --- a/apps/governance-e2e/src/support/staking.functions.ts +++ b/apps/governance-e2e/src/support/staking.functions.ts @@ -182,9 +182,10 @@ export function clickOnValidatorFromList( } else { cy.get(`[row-id="${validatorNumber}"]`) .should('be.visible') - .find(stakeValidatorListName) - .as('validatorOnList'); - cy.get('@validatorOnList').click(); + .first() + .within(() => { + cy.get(stakeValidatorListName).click(); + }); } } From 546b7100938f7a29f600bc080c2af995dabeeee8 Mon Sep 17 00:00:00 2001 From: daro-maj <119658839+daro-maj@users.noreply.github.com> Date: Mon, 21 Aug 2023 09:49:27 +0200 Subject: [PATCH 09/15] test(trading): split and skip deal ticket account tests (#4578) --- .../trading-deal-ticket-basic-submit.cy.ts | 2 +- ...eal-ticket-submit-account-validation.cy.ts | 87 +++++++++++++++++++ .../trading-deal-ticket-submit-account.cy.ts | 2 +- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 apps/trading-e2e/src/integration/trading-deal-ticket-submit-account-validation.cy.ts diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-basic-submit.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-basic-submit.cy.ts index f22f981f4..a5c6df052 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-basic-submit.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-basic-submit.cy.ts @@ -3,7 +3,7 @@ import { testOrderSubmission } from '../support/order-validation'; import type { OrderSubmission } from '@vegaprotocol/wallet'; import { createOrder } from '../support/create-order'; -describe('must submit order', { tags: '@smoke' }, () => { +describe.skip('must submit order', { tags: '@smoke' }, () => { // 7002-SORD-039 before(() => { cy.setVegaWallet(); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account-validation.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account-validation.cy.ts new file mode 100644 index 000000000..af08f16ae --- /dev/null +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account-validation.cy.ts @@ -0,0 +1,87 @@ +import { aliasGQLQuery } from '@vegaprotocol/cypress'; +import { + accountsQuery, + amendGeneralAccountBalance, + amendMarginAccountBalance, +} from '@vegaprotocol/mock'; + +describe.skip( + 'account validation', + { tags: '@regression', testIsolation: true }, + () => { + describe('zero balance error', () => { + beforeEach(() => { + cy.setVegaWallet(); + cy.mockTradingPage(); + let accounts = accountsQuery(); + accounts = amendMarginAccountBalance(accounts, 'market-0', '1000'); + accounts = amendGeneralAccountBalance(accounts, 'market-0', '0'); + cy.mockGQL((req) => { + aliasGQLQuery(req, 'Accounts', accounts); + }); + cy.mockSubscription(); + cy.visit('/#/markets/market-0'); + cy.wait('@Markets'); + }); + + it('should show an error if your balance is zero', () => { + const accounts = accountsQuery(); + amendMarginAccountBalance(accounts, 'market-0', '0'); + cy.mockGQL((req) => { + aliasGQLQuery(req, 'Accounts', accounts); + }); + // 7002-SORD-060 + cy.getByTestId('place-order').should('be.enabled'); + // 7002-SORD-003 + cy.getByTestId('deal-ticket-error-message-zero-balance').should( + 'have.text', + 'You need ' + + 'tDAI' + + ' in your wallet to trade in this market. See all your collateral.Make a deposit' + ); + cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist'); + }); + }); + + describe('not enough balance warning', () => { + beforeEach(() => { + cy.setVegaWallet(); + cy.mockTradingPage(); + let accounts = accountsQuery(); + accounts = amendMarginAccountBalance(accounts, 'market-0', '1000'); + accounts = amendGeneralAccountBalance(accounts, 'market-0', '1'); + cy.mockGQL((req) => { + aliasGQLQuery(req, 'Accounts', accounts); + }); + cy.mockSubscription(); + cy.visit('/#/markets/market-0'); + cy.wait('@Markets'); + + cy.get('[data-testid="deal-ticket-form"]').then(($form) => { + if (!$form.length) { + cy.getByTestId('Order').click(); + } + }); + }); + + it('should display info and button for deposit', () => { + // 7002-SORD-003 + + // warning should show immediately + cy.getByTestId('deal-ticket-warning-margin').should( + 'contain.text', + 'You may not have enough margin available to open this position' + ); + cy.getByTestId('deal-ticket-warning-margin').should( + 'contain.text', + 'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.' + ); + cy.getByTestId('deal-ticket-deposit-dialog-button').click(); + cy.getByTestId('sidebar-content') + .find('h2') + .eq(0) + .should('have.text', 'Deposit'); + }); + }); + } +); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts index 57068bff8..50c311a44 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts @@ -8,7 +8,7 @@ import { import type { OrderSubmission } from '@vegaprotocol/wallet'; import { createOrder } from '../support/create-order'; -describe( +describe.skip( 'account validation', { tags: '@regression', testIsolation: true }, () => { From 5b0bd69710e407f94d55e0bd0462f338467c53b6 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Mon, 21 Aug 2023 10:05:21 +0100 Subject: [PATCH 10/15] fix(governance): better vote error handling (#4579) --- .../components/proposal/proposal.tsx | 3 +- .../vega-transaction-dialog.spec.tsx | 110 ++++++++++++++++++ .../vote-details/vote-buttons.spec.tsx | 9 +- .../components/vote-details/vote-buttons.tsx | 10 +- .../components/vote-details/vote-details.tsx | 5 +- .../vote-details/vote-transaction-dialog.tsx | 8 +- 6 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 apps/governance/src/routes/proposals/components/vote-details/vega-transaction-dialog.spec.tsx diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx index 076ac0fd5..64790df1f 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx @@ -55,7 +55,7 @@ export const Proposal = ({ mostRecentlyEnactedAssociatedMarketProposal, }: ProposalProps) => { const { t } = useTranslation(); - const { submit, Dialog, finalizedVote } = useVoteSubmit(); + const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit(); const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote); if (!proposal) { @@ -215,6 +215,7 @@ export const Proposal = ({ } submit={submit} dialog={Dialog} + transaction={transaction} voteState={voteState} voteDatetime={voteDatetime} /> diff --git a/apps/governance/src/routes/proposals/components/vote-details/vega-transaction-dialog.spec.tsx b/apps/governance/src/routes/proposals/components/vote-details/vega-transaction-dialog.spec.tsx new file mode 100644 index 000000000..6ef513333 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-details/vega-transaction-dialog.spec.tsx @@ -0,0 +1,110 @@ +import { render, screen } from '@testing-library/react'; +import { VoteTransactionDialog } from './vote-transaction-dialog'; +import { VoteState } from './use-user-vote'; +import { VegaTxStatus } from '@vegaprotocol/wallet'; + +describe('VoteTransactionDialog', () => { + const mockTransactionDialog = jest.fn(({ title, content }) => ( +
+
{title}
+
{content?.Complete}
+
+ )); + + it('renders without crashing', () => { + render( + + ); + + expect(screen.getByTestId('vote-transaction-dialog')).toBeInTheDocument(); + }); + + it('renders with txRequested title when voteState is Requested', () => { + render( + + ); + + expect(screen.getByText('txRequested')).toBeInTheDocument(); + }); + + it('renders with votePending title when voteState is Pending', () => { + render( + + ); + + expect(screen.getByText('votePending')).toBeInTheDocument(); + }); + + it('renders with no title when voteState is neither Requested nor Pending', () => { + render( + + ); + + expect(screen.queryByText('txRequested')).not.toBeInTheDocument(); + expect(screen.queryByText('votePending')).not.toBeInTheDocument(); + }); + + it('renders custom error message when voteState is Failed and error message exists', () => { + render( + + ); + + expect(screen.getByText('Custom error test message')).toBeInTheDocument(); + }); + + it('renders default error message when voteState is failed and no error message exists on the tx', () => { + render( + + ); + + expect(screen.getByText('voteError')).toBeInTheDocument(); + }); + + it('renders default ui (i.e. not error) when not in a failed state', () => { + render( + + ); + + expect(screen.queryByText('voteError')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.spec.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.spec.tsx index 7f1ae36b6..9eab79c44 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.spec.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import BigNumber from 'bignumber.js'; import { VoteButtons } from './vote-buttons'; import { VoteState } from './use-user-vote'; @@ -24,6 +24,7 @@ describe('Vote buttons', () => { currentStakeAvailable={new BigNumber(1)} dialog={() =>
Blah
} submit={() => Promise.resolve()} + transaction={null} /> @@ -47,6 +48,7 @@ describe('Vote buttons', () => { currentStakeAvailable={new BigNumber(1)} dialog={() =>
Blah
} submit={() => Promise.resolve()} + transaction={null} /> @@ -81,6 +83,7 @@ describe('Vote buttons', () => { currentStakeAvailable={new BigNumber(1)} dialog={() =>
Blah
} submit={() => Promise.resolve()} + transaction={null} /> @@ -105,6 +108,7 @@ describe('Vote buttons', () => { currentStakeAvailable={new BigNumber(0)} dialog={() =>
Blah
} submit={() => Promise.resolve()} + transaction={null} /> @@ -132,6 +136,7 @@ describe('Vote buttons', () => { currentStakeAvailable={new BigNumber(1)} dialog={() =>
Blah
} submit={() => Promise.resolve()} + transaction={null} /> @@ -159,6 +164,7 @@ describe('Vote buttons', () => { currentStakeAvailable={new BigNumber(10)} dialog={() =>
Blah
} submit={() => Promise.resolve()} + transaction={null} /> @@ -183,6 +189,7 @@ describe('Vote buttons', () => { currentStakeAvailable={new BigNumber(10)} dialog={() =>
Blah
} submit={() => Promise.resolve()} + transaction={null} /> diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx index 919c6b423..7199c9365 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx @@ -17,7 +17,7 @@ import { VoteState } from './use-user-vote'; import { ProposalMinRequirements, ProposalUserAction } from '../shared'; import { VoteTransactionDialog } from './vote-transaction-dialog'; import { useVoteButtonsQuery } from './__generated__/Stake'; -import type { DialogProps } from '@vegaprotocol/wallet'; +import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet'; interface VoteButtonsContainerProps { voteState: VoteState | null; @@ -27,6 +27,7 @@ interface VoteButtonsContainerProps { minVoterBalance: string | null | undefined; spamProtectionMinTokens: string | null | undefined; submit: (voteValue: VoteValue, proposalId: string | null) => Promise; + transaction: VegaTxState | null; dialog: (props: DialogProps) => JSX.Element; className?: string; } @@ -67,6 +68,7 @@ export const VoteButtons = ({ minVoterBalance, spamProtectionMinTokens, submit, + transaction, dialog: Dialog, }: VoteButtonsProps) => { const { t } = useTranslation(); @@ -208,7 +210,11 @@ export const VoteButtons = ({

) )} - + ); }; diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx index db36adfce..b8fbb7c0a 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx @@ -12,7 +12,7 @@ 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 { DialogProps, VegaTxState } from '@vegaprotocol/wallet'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { VoteState } from './use-user-vote'; @@ -22,6 +22,7 @@ interface VoteDetailsProps { minVoterBalance: string | null | undefined; spamProtectionMinTokens: string | null | undefined; proposalType: ProposalType | null; + transaction: VegaTxState | null; submit: (voteValue: VoteValue, proposalId: string | null) => Promise; dialog: (props: DialogProps) => JSX.Element; voteState: VoteState | null; @@ -34,6 +35,7 @@ export const VoteDetails = ({ spamProtectionMinTokens, proposalType, submit, + transaction, dialog, voteState, voteDatetime, @@ -228,6 +230,7 @@ export const VoteDetails = ({ spamProtectionMinTokens={spamProtectionMinTokens} className="flex" submit={submit} + transaction={transaction} dialog={dialog} /> ) diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-transaction-dialog.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-transaction-dialog.tsx index 59f04d705..c0d9b3da9 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-transaction-dialog.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/vote-transaction-dialog.tsx @@ -1,9 +1,10 @@ import { t } from '@vegaprotocol/i18n'; import { VoteState } from './use-user-vote'; -import type { DialogProps } from '@vegaprotocol/wallet'; +import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet'; interface VoteTransactionDialogProps { voteState: VoteState; + transaction: VegaTxState | null; TransactionDialog: (props: DialogProps) => JSX.Element; } @@ -20,12 +21,15 @@ const dialogTitle = (voteState: VoteState): string | undefined => { export const VoteTransactionDialog = ({ voteState, + transaction, TransactionDialog, }: VoteTransactionDialogProps) => { // Render a custom message if the voting fails otherwise // pass undefined so that the default vega transaction dialog UI gets used const customMessage = - voteState === VoteState.Failed ?

{t('voteError')}

: undefined; + voteState === VoteState.Failed ? ( +

{transaction?.error?.message || t('voteError')}

+ ) : undefined; return (
From 206c6c7207d8d99d9f1348f4ceb43981fa303d30 Mon Sep 17 00:00:00 2001 From: Edd Date: Mon, 21 Aug 2023 18:38:32 +0100 Subject: [PATCH 11/15] fix(explorer): check linking type when summing stake --- .../routes/parties/id/Party-assets.graphql | 35 ++++++++++++++++++- .../parties/id/__generated__/Party-assets.ts | 6 ++-- .../id/components/party-block-stake.tsx | 12 +++++-- .../__generated__/Orders.ts | 6 ++-- libs/types/src/__generated__/types.ts | 4 ++- 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/apps/explorer/src/app/routes/parties/id/Party-assets.graphql b/apps/explorer/src/app/routes/parties/id/Party-assets.graphql index 72ae0fa38..6d30cc986 100644 --- a/apps/explorer/src/app/routes/parties/id/Party-assets.graphql +++ b/apps/explorer/src/app/routes/parties/id/Party-assets.graphql @@ -29,6 +29,37 @@ fragment ExplorerPartyAssetsAccounts on AccountBalance { } } +fragment ExplorerPartyLinks on AccountBalance { + asset { + name + id + decimals + symbol + source { + __typename + ... on ERC20 { + contractAddress + } + } + } + type + balance + market { + id + decimalPlaces + tradableInstrument { + instrument { + name + product { + ... on Future { + quoteName + } + } + } + } + } +} + query ExplorerPartyAssets($partyId: ID!) { partiesConnection(id: $partyId) { edges { @@ -48,9 +79,11 @@ query ExplorerPartyAssets($partyId: ID!) { } stakingSummary { currentStakeAvailable - linkings(pagination: { first: 100 }) { + linkings(pagination: { last: 100 }) { edges { node { + type + status amount } } diff --git a/apps/explorer/src/app/routes/parties/id/__generated__/Party-assets.ts b/apps/explorer/src/app/routes/parties/id/__generated__/Party-assets.ts index 77e334201..1f6b6262d 100644 --- a/apps/explorer/src/app/routes/parties/id/__generated__/Party-assets.ts +++ b/apps/explorer/src/app/routes/parties/id/__generated__/Party-assets.ts @@ -10,7 +10,7 @@ export type ExplorerPartyAssetsQueryVariables = Types.Exact<{ }>; -export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null }; +export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', type: Types.StakeLinkingType, status: Types.StakeLinkingStatus, amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null }; export const ExplorerPartyAssetsAccountsFragmentDoc = gql` fragment ExplorerPartyAssetsAccounts on AccountBalance { @@ -67,6 +67,8 @@ export const ExplorerPartyAssetsDocument = gql` linkings(pagination: {first: 100}) { edges { node { + type + status amount } } @@ -111,4 +113,4 @@ export function useExplorerPartyAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHo } export type ExplorerPartyAssetsQueryHookResult = ReturnType; export type ExplorerPartyAssetsLazyQueryHookResult = ReturnType; -export type ExplorerPartyAssetsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type ExplorerPartyAssetsQueryResult = Apollo.QueryResult; diff --git a/apps/explorer/src/app/routes/parties/id/components/party-block-stake.tsx b/apps/explorer/src/app/routes/parties/id/components/party-block-stake.tsx index 831c15ccc..5121f1ff1 100644 --- a/apps/explorer/src/app/routes/parties/id/components/party-block-stake.tsx +++ b/apps/explorer/src/app/routes/parties/id/components/party-block-stake.tsx @@ -42,9 +42,15 @@ export const PartyBlockStake = ({ linkedLength && linkedLength > 0 ? p?.stakingSummary?.linkings?.edges ?.reduce((total, e) => { - return new BigNumber(total).plus( - new BigNumber(e?.node.amount || 0) - ); + const accumulator = new BigNumber(total) + const diff = new BigNumber(e?.node.amount || 0) + if (e?.node.type === 'TYPE_LINK') { + return accumulator.plus(diff); + } else if (e?.node.type === 'TYPE_UNLINK') { + return accumulator.minus(diff); + } else { + return accumulator + } }, new BigNumber(0)) .toString() : '0'; diff --git a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts index 04afda855..a4513725e 100644 --- a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts +++ b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts @@ -34,21 +34,21 @@ export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: A export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null }; -export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } }; +export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } }; export type StopOrdersQueryVariables = Types.Exact<{ partyId: Types.Scalars['ID']; }>; -export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null }; +export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null }; export type StopOrderByIdQueryVariables = Types.Exact<{ stopOrderId: Types.Scalars['ID']; }>; -export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }; +export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }; export const OrderFieldsFragmentDoc = gql` fragment OrderFields on Order { diff --git a/libs/types/src/__generated__/types.ts b/libs/types/src/__generated__/types.ts index 18605c0bd..28441bfcf 100644 --- a/libs/types/src/__generated__/types.ts +++ b/libs/types/src/__generated__/types.ts @@ -4364,6 +4364,8 @@ export type StopOrder = { marketId: Scalars['ID']; /** If OCO (one-cancels-other) order, the ID of the associated order. */ ocoLinkId?: Maybe; + /** The order that was created when triggered. */ + order?: Maybe; /** Party that submitted the stop order. */ partyId: Scalars['ID']; /** Status of the stop order */ @@ -4371,7 +4373,7 @@ export type StopOrder = { /** Order to submit when the stop order is triggered. */ submission: OrderSubmission; /** Price movement that will trigger the stop order */ - trigger?: Maybe; + trigger: StopOrderTrigger; /** Direction the price is moving to trigger the stop order. */ triggerDirection: StopOrderTriggerDirection; /** Time the stop order was last updated. */ From fa1825ca64317d02171531911a198f2898d0b705 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 22 Aug 2023 08:29:13 +0100 Subject: [PATCH 12/15] chore(trading): remove settings test (#4580) --- .../src/integration/settings.cy.ts | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 apps/trading-e2e/src/integration/settings.cy.ts diff --git a/apps/trading-e2e/src/integration/settings.cy.ts b/apps/trading-e2e/src/integration/settings.cy.ts deleted file mode 100644 index a09223e5e..000000000 --- a/apps/trading-e2e/src/integration/settings.cy.ts +++ /dev/null @@ -1,30 +0,0 @@ -describe('Settings page', { tags: '@smoke' }, () => { - beforeEach(() => { - cy.clearLocalStorage(); - - cy.mockTradingPage(); - cy.mockSubscription(); - cy.setOnBoardingViewed(); - cy.visit('/'); - - // Only click if not already active otherwise sidebar will close - cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => { - if ($sidebarContent.find('h2').text() !== 'Settings') { - cy.get('[data-testid="sidebar"] [data-testid="Settings"]').click(); - } - }); - }); - - it('telemetry checkbox should work well', () => { - const telemetrySwitch = '#switch-settings-telemetry-switch'; - cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked'); - cy.get(telemetrySwitch).click(); - cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked'); - cy.reload(); - cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked'); - cy.get(telemetrySwitch).click(); - cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked'); - cy.reload(); - cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked'); - }); -}); From ab4f4e9084be8c466bd0ad2f72a409b80ef6f9c5 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 22 Aug 2023 08:29:47 +0100 Subject: [PATCH 13/15] chore(trading): remove trading chart tests (#4582) --- .../src/integration/trading-chart.cy.ts | 186 ------------------ 1 file changed, 186 deletions(-) delete mode 100644 apps/trading-e2e/src/integration/trading-chart.cy.ts diff --git a/apps/trading-e2e/src/integration/trading-chart.cy.ts b/apps/trading-e2e/src/integration/trading-chart.cy.ts deleted file mode 100644 index 152ba334d..000000000 --- a/apps/trading-e2e/src/integration/trading-chart.cy.ts +++ /dev/null @@ -1,186 +0,0 @@ -interface ItemInfoType { - name: string; - infoText: string; -} - -type CheckMenuItemsFnType = ( - triggerSelector: string, - validTexts: string[], - clickItem?: string -) => void; -type CheckMenuItemCheckboxFnType = ( - buttonText: string, - items: ItemInfoType[] -) => void; - -const menuItemRadio = 'div[role="menuitemradio"]'; -const menuItemCheckbox = 'div[role="menuitemcheckbox"]'; -const button = 'button'; -const indicatorInfo = '.indicator-info-wrapper'; - -const checkMenuItems: CheckMenuItemsFnType = ( - triggerSelector, - validTexts, - clickItem -) => { - cy.get(triggerSelector).click(); - - cy.get(menuItemRadio) - .should('have.length', validTexts.length) - .each(($el, index) => { - const text = $el.text().trim(); - expect(text).to.equal(validTexts[index]); - }); - - if (clickItem) { - cy.contains(menuItemRadio, clickItem).click(); - cy.get(triggerSelector).click(); - cy.get(`${menuItemRadio}[data-state="checked"]`) - .invoke('text') - .then((text: string) => { - expect(text.trim()).to.equal(clickItem); - }); - } -}; - -const checkMenuItemCheckbox: CheckMenuItemCheckboxFnType = ( - buttonText, - items -) => { - items.forEach((item) => { - cy.contains(button, buttonText).click(); - cy.contains(menuItemCheckbox, item.name).click(); - }); - - cy.contains(button, buttonText).click(); - cy.get(menuItemCheckbox) - .should('have.length', items.length) - .each(($el, index) => { - const text = $el.text(); - expect(text).to.equal(items[index].name); - }); - - items.forEach((item, index) => { - cy.get(indicatorInfo) - .eq(index + 1) - .invoke('text') - .should('eq', item.infoText); - }); - - cy.contains(button, buttonText).click({ force: true }); -}; - -function getButtonSelectorByText(text: string): string { - return `${button}[aria-haspopup="menu"]:contains(${text})`; -} - -beforeEach(() => { - cy.mockTradingPage(); - cy.mockSubscription(); - cy.setOnBoardingViewed(); - cy.visit('/#/markets/market-0'); - cy.wait('@Markets'); -}); - -describe( - 'chart display options', - { tags: '@smoke', testIsolation: true }, - () => { - it('change time interval', () => { - // 6004-CHAR-001 - checkMenuItems( - getButtonSelectorByText('Interval:'), - ['1m', '5m', '15m', '1H', '6H', '1D'], - '1m' - ); - }); - - it('change display type', () => { - // 6004-CHAR-002 - // 6004-CHAR-003 - checkMenuItems( - '[aria-label$="chart icon"]', - ['Mountain', 'Candlestick', 'Line', 'OHLC'], - 'Mountain' - ); - }); - - it('Overlays', () => { - // 6004-CHAR-004 - // 6004-CHAR-008 - // 6004-CHAR-009 - // 6004-CHAR-034 - // 6004-CHAR-037 - // 6004-CHAR-039 - // 6004-CHAR-041 - - const overlayInfo: ItemInfoType[] = [ - { - name: 'Bollinger bands', - infoText: 'Bollinger: Upper 174.78590Lower 173.38014', - }, - { - name: 'Envelope', - infoText: 'Envelope: Upper 191.29000Lower 156.51000', - }, - { name: 'EMA', infoText: 'EMA: 174.06793' }, - { name: 'Moving average', infoText: 'Moving average: 174.08302' }, - { - name: 'Price monitoring bounds', - infoText: - 'Price Monitoring Bounds 1: Min 162.56291Max 182.96869Reference 172.47489', - }, - ]; - - checkMenuItemCheckbox('Overlays', overlayInfo); - }); - - it('Studies', () => { - // 6004-CHAR-005 - // 6004-CHAR-006 - // 6004-CHAR-007 - // 6004-CHAR-042 - // 6004-CHAR-045 - // 6004-CHAR-047 - // 6004-CHAR-049 - // 6004-CHAR-051 - const studyInfo: ItemInfoType[] = [ - { - name: 'Eldar-ray', - infoText: 'Eldar-ray: Bull -0.08376Bear -0.58376', - }, - { name: 'Force index', infoText: 'Force index: 987.48858' }, - { name: 'MACD', infoText: 'MACD: S -0.06420D 0.00359MACD -0.06062' }, - { name: 'RSI', infoText: 'RSI: 47.08648' }, - { name: 'Volume', infoText: 'Volume: 55,000' }, - ]; - cy.get(indicatorInfo).eq(1).realHover(); - cy.get('.chart__wrapper [data-testid="split-view-view"]') - .last() - .find('[role="button"][title="Close"]') - .click({ force: true }); - cy.get(indicatorInfo).should('have.length', 1); - - checkMenuItemCheckbox('Studies', studyInfo); - }); - - it('price details', () => { - // 6004-CHAR-010 - const expectedDateRegex = new RegExp( - /^\d{2}:\d{2} \d{2} [A-Za-z]{3} \d{4}$/ - ); - const expectedOhlc = `O 173.60000H 174.00000L 173.50000C 173.90000Change −0.60000(−0.34%)`; - cy.get(indicatorInfo) - .eq(0) - .invoke('text') - .then((text) => { - const actualDate = text.slice(0, -67); - // eslint-disable-next-line no-console - console.log(actualDate); - const actualOhlc = text.slice(-67); - assert.isTrue(expectedDateRegex.test(actualDate)); - assert.strictEqual(actualOhlc, expectedOhlc); - }); - }); - } -); From 6d130c9cfc6b19c384cb1f35e91e2d04f57ff1c0 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Tue, 22 Aug 2023 11:17:10 +0200 Subject: [PATCH 14/15] feat(positions): filter closed markets in positions table (#4569) --- .../client-pages/market/trade-grid.tsx | 6 +++- .../client-pages/market/trade-views.tsx | 7 +++- .../positions-container.tsx | 33 ++++++++++++++++--- .../components/positions-menu/index.ts | 1 + .../positions-menu/positions-menu.tsx | 18 ++++++++++ .../src/lib/positions-data-providers.spec.ts | 32 +++++++++++++++++- .../src/lib/positions-data-providers.ts | 28 ++++++++++++++-- libs/positions/src/lib/positions-manager.tsx | 6 ++-- .../src/lib/positions-table.spec.tsx | 1 + libs/positions/src/lib/positions-table.tsx | 4 +-- specs/7004-POSI-positions.md | 6 +++- 11 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 apps/trading/components/positions-menu/index.ts create mode 100644 apps/trading/components/positions-menu/positions-menu.tsx diff --git a/apps/trading/client-pages/market/trade-grid.tsx b/apps/trading/client-pages/market/trade-grid.tsx index b316a3f54..ab64f77b1 100644 --- a/apps/trading/client-pages/market/trade-grid.tsx +++ b/apps/trading/client-pages/market/trade-grid.tsx @@ -94,7 +94,11 @@ const MainGrid = memo( > - + } + > ( diff --git a/apps/trading/components/positions-container/positions-container.tsx b/apps/trading/components/positions-container/positions-container.tsx index 518f7c2d6..f830d4666 100644 --- a/apps/trading/components/positions-container/positions-container.tsx +++ b/apps/trading/components/positions-container/positions-container.tsx @@ -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()( - persist(createDataGridSlice, { - name: 'vega_positions_store', - }) +type PositionsStoreSlice = { + showClosedMarkets: boolean; + toggleClosedMarkets: () => void; +}; + +const createPositionStoreSlice: StateCreator = (set) => ({ + showClosedMarkets: false, + toggleClosedMarkets: () => { + set((curr) => { + return { + showClosedMarkets: !curr.showClosedMarkets, + }; + }); + }, +}); + +export const usePositionsStore = create()( + persist( + (...args) => ({ + ...createPositionStoreSlice(...args), + ...createDataGridSlice(...args), + }), + { + name: 'vega_positions_store', + } + ) ); diff --git a/apps/trading/components/positions-menu/index.ts b/apps/trading/components/positions-menu/index.ts new file mode 100644 index 000000000..12fc9ea53 --- /dev/null +++ b/apps/trading/components/positions-menu/index.ts @@ -0,0 +1 @@ +export * from './positions-menu'; diff --git a/apps/trading/components/positions-menu/positions-menu.tsx b/apps/trading/components/positions-menu/positions-menu.tsx new file mode 100644 index 000000000..58dddee7b --- /dev/null +++ b/apps/trading/components/positions-menu/positions-menu.tsx @@ -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 ( + + {showClosed ? t('Hide closed markets') : t('Show closed markets')} + + ); +}; diff --git a/libs/positions/src/lib/positions-data-providers.spec.ts b/libs/positions/src/lib/positions-data-providers.spec.ts index a8ca0aaa9..6c1f67093 100644 --- a/libs/positions/src/lib/positions-data-providers.spec.ts +++ b/libs/positions/src/lib/positions-data-providers.spec.ts @@ -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 = [ @@ -223,4 +228,29 @@ describe('getMetrics && rejoinPositionData', () => { ); expect(metrics[1].status).toEqual(positions[1].positionStatus); }); + + it('sorts and filters positions', () => { + const createPosition = (override?: Partial) => + ({ + 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']); + }); }); diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts index 8d44c0941..d5c533b83 100644 --- a/libs/positions/src/lib/positions-data-providers.ts +++ b/libs/positions/src/lib/positions-data-providers.ts @@ -41,6 +41,7 @@ export interface Position { marketId: string; marketCode: string; marketTradingMode: Schema.MarketTradingMode; + marketState: Schema.MarketState; markPrice: string | undefined; notional: string | undefined; openVolume: string; @@ -119,6 +120,7 @@ export const getMetrics = ( marketId: market.id, marketCode: market.tradableInstrument.instrument.code, marketTradingMode: market.tradingMode, + marketState: market.state, markPrice: marketData ? marketData.markPrice : undefined, notional: notional ? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0) @@ -248,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, @@ -265,7 +287,7 @@ export const positionsMarketsProvider = makeDerivedDataProvider< export const positionsMetricsProvider = makeDerivedDataProvider< Position[], Position[], - PositionsQueryVariables & { marketIds: string[] } + PositionsQueryVariables & { marketIds: string[]; showClosed: boolean } >( [ (callback, client, variables) => @@ -281,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, 'marketCode'); + return preparePositions(metrics, variables.showClosed); }, (data, delta, previousData) => data.filter((row) => { diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index f01f71cfa..521ae1b45 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -16,6 +16,7 @@ interface PositionsManagerProps { onMarketClick?: (marketId: string) => void; isReadOnly: boolean; gridProps?: ReturnType; + 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 = ({ ); }, - minWidth: 75, - maxWidth: 75, + minWidth: 55, + maxWidth: 55, } : null, ]; diff --git a/specs/7004-POSI-positions.md b/specs/7004-POSI-positions.md index 3bcb69493..5965f24f8 100644 --- a/specs/7004-POSI-positions.md +++ b/specs/7004-POSI-positions.md @@ -51,4 +51,8 @@ - **Must** be able to see if your realised PnL was affected by loss socialisation (7004-POSI-018) -- **Must** Must be able to see what type of product the position was opened on (7004-POSI-019) +- **Must** be able to see what type of product the position was opened on (7004-POSI-019) + +- **Must** not see positions on markets which are closed (7004-POSI-020) + +- **Must** be able to show closed markets (7004-POSI-021) From d2854b6e90cdf9c1a2436eb54a77d9783d36bfa2 Mon Sep 17 00:00:00 2001 From: Joe Tsang <30622993+jtsang586@users.noreply.github.com> Date: Tue, 22 Aug 2023 12:06:41 +0100 Subject: [PATCH 15/15] chore(governance): add acs for network nodes (#4577) --- .../src/integration/view/home.cy.ts | 33 +++++++++++++++++++ specs/1005-VEST-vesting.md | 1 - 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/governance-e2e/src/integration/view/home.cy.ts b/apps/governance-e2e/src/integration/view/home.cy.ts index a84b15846..dc080b0ad 100644 --- a/apps/governance-e2e/src/integration/view/home.cy.ts +++ b/apps/governance-e2e/src/integration/view/home.cy.ts @@ -119,6 +119,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () { }); }); + // 0006-NETW-001 0006-NETW-002 it('should display network data', function () { cy.getByTestId('git-network-data') .should('contain.text', 'Reading network data from') @@ -130,6 +131,37 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () { }); }); + // 0006-NETW-003 0006-NETW-008 0006-NETW-009 0006-NETW-010 0006-NETW-012 0006-NETW-013 0006-NETW-017 0006-NETW-018 0006-NETW-019 0006-NETW-020 + it('should have option to switch to different network node', function () { + cy.getByTestId('git-network-data').within(() => { + cy.getByTestId('link').click(); + }); + cy.getByTestId('node-row').within(() => { + cy.getByTestId('node-url-0') + .parent() + .should('have.text', 'http://localhost:3008/graphql'); + cy.getByTestId('response-time-cell') + .invoke('text') + .should('not.be.empty') + .and('not.eq', 'Checking'); + cy.getByTestId('block-height-cell') + .invoke('text') + .should('not.be.empty') + .then((currentBlockHeight) => { + // Check that block height updates automatically + cy.getByTestId('block-height-cell') + .invoke('text') + .should('not.eq', currentBlockHeight); + }); + cy.getByTestId('subscription-cell').should('have.text', 'Yes'); + }); + cy.getByTestId('connect').should('be.disabled'); + cy.getByTestId('node-url-custom').click(); + cy.get('input').should('exist'); + cy.getByTestId('connect').should('be.disabled'); + cy.getByTestId('icon-cross').click(); + }); + it('should display eth data', function () { cy.getByTestId('git-eth-data') .should('contain.text', 'Reading Ethereum data from') @@ -138,6 +170,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () { }); }); + // 0006-NETW-011 it('should contain link for known issues on Github', function () { cy.getByTestId('git-info').within(() => { cy.contains('Known issues and feedback on') diff --git a/specs/1005-VEST-vesting.md b/specs/1005-VEST-vesting.md index 819c4d4b2..23157e85b 100644 --- a/specs/1005-VEST-vesting.md +++ b/specs/1005-VEST-vesting.md @@ -53,7 +53,6 @@ for the a given Ethereum wallet/address/key: - **must** see how many tokens in each tranche are locked 1005-VEST-025 - **must** see how many tokens in each tranche are redeemable 1005-VEST-026 - **must** see an option to redeem from tranche 1005-VEST-027 - - **must** be warned if amount that can be redeemed from that tranche is greater than the un-associated balance for that Eth key (because this will cause the redeem function to fail) 1005-VEST-028 - **should** see how many tokens I'd need to disassociate to be able to run the redeem function (this should be rounded up to avoid the transaction failing due to more tokens having unlocked since the user looked at the form) - **should** see link to [disassociate](1004-ASSO-associate.md)