Compare commits

..
65 changed files with 867 additions and 1546 deletions
+18 -39
View File
@@ -173,7 +173,7 @@ jobs:
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -238,31 +238,21 @@ jobs:
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
run: |
if echo ${{ github.ref }} | grep -q main; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
fleek_id='f8f2e051-f18e-49e6-b876-0a39369dc0d8'
elif echo ${{ github.ref }} | grep -q release/testnet; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
https://api.fleek.co/graphql
fleek_id='79baaeca-1952-4ae7-a256-f668cfc1d68e'
fi
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"$fleek_id\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"$fleek_id\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
@@ -329,27 +319,16 @@ jobs:
new_console_url_domain=cf-ipfs.com
# new_console_url_domain=dweb.link
new_console_url="http://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
new_console_url="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
echo "new_console_url=${new_console_url}"
# Update record in DNSimple
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
dnsimple_account_id=84895
dnsimple_zone_name=fairground.wtf
dnsimple_record_id=45300615
# see: https://dnsimple.com/a/84895/domains/fairground.wtf/records/45300615/edit
curl --fail -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-X PATCH \
-d "{
\"content\": \"${new_console_url}\"
}" \
https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
# trading.fairground.wtf
dnsimple_record_id=45302531
curl --fail -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
dnsimple_record_id=44571545
# see: https://dnsimple.com/a/84895/domains/fairground.wtf/records/44571545/edit
curl -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-X PATCH \
@@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment>) => {
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
@@ -14,16 +14,9 @@ import {
submitUniqueRawProposal,
voteForProposal,
} from '../../../../governance-e2e/src/support/governance.functions';
import {
ensureSpecifiedUnstakedTokensAreAssociated,
stakingPageAssociateTokens,
stakingPageDisassociateAllTokens,
} from '../../../../governance-e2e/src/support/staking.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../../../governance-e2e/src/support/staking.functions';
import { ethereumWalletConnect } from '../../../../governance-e2e/src/support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletSetSpecifiedApprovalAmount,
} from '../../support/wallet-functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../../../governance-e2e/src/support/wallet-teardown.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
import { formatDateWithLocalTimezone } from '@vegaprotocol/utils';
@@ -51,7 +44,7 @@ describe(
before('connect wallets and set approval limit', function () {
cy.visit('/');
ethereumWalletConnect();
// cy.associateTokensToVegaWallet('1');
cy.associateTokensToVegaWallet('1');
});
beforeEach('visit proposals tab', function () {
@@ -304,33 +297,5 @@ describe(
.and('be.visible');
});
});
it('Able to vote for proposal twice by switching public key', function () {
ensureSpecifiedUnstakedTokensAreAssociated('1');
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
voteForProposal('for');
cy.contains('You voted: For').should('be.visible');
ethereumWalletConnect();
switchVegaWalletPubKey();
stakingPageAssociateTokens('2');
navigateTo(navigation.proposals);
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
cy.getByTestId('you-voted').should('not.exist');
voteForProposal('against');
cy.contains('You voted: Against').should('be.visible');
switchVegaWalletPubKey();
cy.get(proposalVoteProgressForTokens).should('contain.text', '1.00');
// Checking vote status for different public keys is displayed correctly
cy.contains('You voted: For').should('be.visible');
});
switchVegaWalletPubKey();
stakingPageDisassociateAllTokens();
});
}
);
@@ -16,7 +16,7 @@ import {
} from '../../support/proposal.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const closedProposals = '[data-testid="closed-proposals"]';
@@ -32,7 +32,7 @@ import {
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
@@ -23,11 +23,10 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const openProposals = '[data-testid="open-proposals"]';
@@ -270,7 +269,8 @@ context(
// Will fail if run after 'Able to submit update market proposal and vote for proposal'
// 3002-PROP-022
it('Unable to submit update market proposal without equity-like share in the market', function () {
switchVegaWalletPubKey();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click(); // switch to second wallet pub key
stakingPageAssociateTokens('1');
goToMakeNewProposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal - rejected');
@@ -300,7 +300,8 @@ context(
closeDialog();
ethereumWalletConnect();
stakingPageDisassociateAllTokens();
switchVegaWalletPubKey();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
});
// 3002-PROP-020
@@ -21,7 +21,7 @@ import {
} from '../../support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const proposalListItem = 'proposals-list-item';
const openProposals = '[data-testid="open-proposals"]';
@@ -14,7 +14,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
depositAsset,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de';
const vegaWalletUnstakedBalance =
@@ -25,7 +25,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
@@ -14,12 +14,11 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletAssociate,
vegaWalletDisassociate,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
@@ -302,7 +301,8 @@ context(
Cypress.env('vegaWalletPublicKey')
);
switchVegaWalletPubKey();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
cy.get(connectedVegaKey).should(
'have.text',
Cypress.env('vegaWalletPublicKey2')
@@ -5,7 +5,7 @@ import {
waitForSpinner,
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { depositAsset } from '../../support/wallet-functions';
import { depositAsset } from '../../support/wallet-teardown.functions';
const withdraw = 'withdraw';
const withdrawalForm = 'withdraw-form';
@@ -11,7 +11,7 @@ import {
goToMakeNewProposal,
governanceProposalType,
} from '../../support/governance.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey2');
const vegaPubkeyTruncated = Cypress.env('vegaWalletPublicKey2Short');
@@ -1,9 +1,7 @@
import { truncateByChars } from '@vegaprotocol/utils';
import { waitForSpinner } from '../../support/common.functions';
import {
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown,
} from '../../support/wallet-functions';
import { vegaWalletTeardown } from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
+2 -1
View File
@@ -5,7 +5,8 @@ import './common.functions.ts';
import './staking.functions.ts';
import './governance.functions.ts';
import './wallet-eth.functions.ts';
import './wallet-functions.ts';
import './wallet-teardown.functions.ts';
import './wallet-vega.functions.ts';
import './proposal.functions.ts';
import 'cypress-mochawesome-reporter/register';
import registerCypressGrep from '@cypress/grep';
@@ -1,5 +1,5 @@
import { closeDialog } from './common.functions';
import { vegaWalletTeardown } from './wallet-functions';
import { vegaWalletTeardown } from './wallet-teardown.functions';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
@@ -173,27 +173,3 @@ export async function vegaWalletDisassociate(amount: string) {
amount = amount + '0'.repeat(18);
stakingBridgeContract.remove_stake(amount, vegaWalletPubKey);
}
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
export function switchVegaWalletPubKey() {
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
}
@@ -0,0 +1,18 @@
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
@@ -596,8 +596,6 @@
"noPercentage": "No percentage",
"proposalJson": "Full proposal JSON",
"proposalDetails": "Proposal details",
"marketSpecification": "Market specification",
"viewMarketJson": "View market JSON",
"proposalDescription": "Description",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
@@ -731,11 +729,7 @@
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
"Hours": "hours",
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: 2 minutes of extra time are added when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "The proposal will fail if enactment is earlier than the voting deadline",
"ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline": "The proposal will fail if enactment deadline is below the minimum",
"ProposalWillFailIfVotingIsBelowTheMinimumDeadline": "The proposal will fail if voting deadline is below the minimum",
"ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline": "The proposal will fail if enactment deadline is above the maximum",
"ProposalWillFailIfVotingIsAboveTheMaximumDeadline": "The proposal will fail if voting deadline is above the maximum",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline",
"SelectAMarketToChange": "Select a market to change",
"MarketName": "Market name",
"MarketCode": "Market code",
@@ -1,6 +0,0 @@
import classnames from 'classnames';
export const collapsibleToggleStyles = (toggleState: boolean) =>
classnames('mb-4 transition-transform ease-in-out duration-300', {
'rotate-180': toggleState,
});
@@ -1,9 +1,9 @@
import ReactMarkdown from 'react-markdown';
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
export const ProposalDescription = ({
description,
@@ -12,6 +12,9 @@ export const ProposalDescription = ({
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
const showDescriptionIconClasses = classnames('mb-4', {
'rotate-180': showDescription,
});
return (
<section data-testid="proposal-description">
@@ -21,7 +24,7 @@ export const ProposalDescription = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDescription')} />
<div className={collapsibleToggleStyles(showDescription)}>
<div className={showDescriptionIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1,8 +1,8 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -13,6 +13,9 @@ export const ProposalJson = ({
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<section data-testid="proposal-json">
@@ -22,7 +25,7 @@ export const ProposalJson = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalJson')} />
<div className={collapsibleToggleStyles(showDetails)}>
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1 +0,0 @@
export * from './proposal-market-data';
@@ -1,222 +0,0 @@
import isEqual from 'lodash/isEqual';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityMonitoringParametersInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
Icon,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import { create } from 'zustand';
type MarketDataDialogState = {
isOpen: boolean;
open: () => void;
close: () => void;
};
export const useMarketDataDialogStore = create<MarketDataDialogState>(
(set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
})
);
export const ProposalMarketData = ({
marketData,
}: {
marketData: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
if (!marketData) {
return null;
}
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
return (
<section className="relative" data-testid="proposal-market-data">
<button
onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-market-data-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('marketSpecification')} />
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && (
<>
<div className="float-right">
<Button onClick={open} data-testid="view-market-json">
{t('viewMarketJson')}
</Button>
</div>
<div className="mb-10">
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
) : (
<>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
}
/>
</>
)}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
/>
}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
/>
</Accordion>
</div>
</>
)}
<Dialog
title={marketData.tradableInstrument.instrument.code}
open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())}
size="medium"
dataTestId="market-json-dialog"
>
<CopyWithTooltip text={JSON.stringify(marketData)}>
<button className="bg-vega-dark-100 rounded-sm py-2 px-3 mb-4 text-white">
<span>
<Icon name="duplicate" />
</span>
<span className="ml-2">Copy</span>
</button>
</CopyWithTooltip>
<SyntaxHighlighter data={marketData} />
</Dialog>
</section>
);
};
@@ -1,3 +1,4 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -12,7 +13,6 @@ import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -57,6 +57,10 @@ export const ProposalVotesTable = ({
? t('byTokenVote')
: t('byLiquidityVote');
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<>
<button
@@ -65,7 +69,7 @@ export const ProposalVotesTable = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('voteBreakdown')} />
<div className={collapsibleToggleStyles(showDetails)}>
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -29,6 +29,9 @@ jest.mock('../proposal-change-table', () => ({
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-terms/proposal-terms', () => ({
ProposalTerms: () => <div data-testid="proposal-terms"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
}));
@@ -71,6 +74,7 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-terms')).toBeInTheDocument();
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
@@ -1,22 +1,22 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalTerms } from '../proposal-terms';
import { ProposalVotesTable } from '../proposal-votes-table';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import { Link } from 'react-router-dom';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import React from 'react';
import { useTranslation } from 'react-i18next';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@@ -28,16 +28,11 @@ export enum ProposalType {
}
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
newMarketData?: MarketInfoWithData | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
}
export const Proposal = ({
proposal,
restData,
newMarketData,
}: ProposalProps) => {
export const Proposal = ({ proposal, restData }: ProposalProps) => {
const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
@@ -102,58 +97,51 @@ export const Proposal = ({
</div>
<ProposalHeader proposal={proposal} isListItem={false} />
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={
proposal.terms.change.source.withdrawThreshold
}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' &&
proposal.terms.change.__typename !== 'NewFreeform' && (
<div className="mb-4">
<ProposalMarketData marketData={newMarketData} />
<ProposalTerms data={proposal.terms} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</div>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</section>
</AsyncRenderer>
@@ -224,47 +224,7 @@ describe('Proposal form vote, validation and enactment deadline', () => {
expect(
screen.getByTestId('enactment-before-voting-deadline')
).toHaveTextContent(
'The proposal will fail if enactment is earlier than the voting deadline'
);
});
it('displays error text if the vote deadline is set earlier than the minimum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('voting-less-than-min')).toHaveTextContent(
'The proposal will fail if voting deadline is below the minimum'
);
});
it('displays error text if the vote deadline is set later than the maximum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('voting-greater-than-max')).toHaveTextContent(
'The proposal will fail if voting deadline is above the maximum'
);
});
it('displays error text if the enactment deadline is set earlier than the minimum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('enactment-less-than-min')).toHaveTextContent(
'The proposal will fail if enactment deadline is below the minimum'
);
});
it('displays error text if the enactment deadline is set later than the maximum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('enactment-greater-than-max')).toHaveTextContent(
'The proposal will fail if enactment deadline is above the maximum'
'Proposal will fail if enactment is earlier than the voting deadline'
);
});
});
@@ -220,41 +220,21 @@ const EnactmentForm = ({
<span data-testid="enactment-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.enactment)}
</span>
{deadlines.enactment && (
<>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
{deadlines.enactment < minEnactmentHours && (
<span
data-testid="enactment-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.enactment > maxEnactmentHours && (
<span
data-testid="enactment-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline')}
</span>
)}
</>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment && deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
</p>
)}
@@ -520,33 +500,13 @@ export function ProposalFormVoteAndEnactmentDeadline({
<span data-testid="voting-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.vote)}
</span>
{deadlines.vote && (
<>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.vote < minVoteHours && (
<span
data-testid="voting-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.vote > maxVoteHours && (
<span
data-testid="voting-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsAboveTheMaximumDeadline')}
</span>
)}
</>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
</p>
)}
@@ -1,10 +1,11 @@
import { captureMessage } from '@sentry/minimal';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useState } from 'react';
import { useUserVoteQuery } from './__generated__/Vote';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { FinalizedVote } from '@vegaprotocol/proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
export enum VoteState {
NotCast = 'NotCast',
@@ -44,23 +45,21 @@ export function useUserVote(
);
useEffect(() => {
if (finalizedVote?.vote.value && finalizedVote.pubKey === pubKey) {
if (finalizedVote?.vote.value) {
setUserVote(finalizedVote);
} else if (data?.party?.votesConnection?.edges && pubKey) {
const vote = removePaginationWrapper(
data?.party?.votesConnection?.edges
).find(({ proposalId: pId }) => proposalId === pId);
if (vote) {
setUserVote({ ...vote, pubKey });
}
} else if (data?.party?.votesConnection?.edges) {
// This sets the vote (if any) when the user first loads the page
setUserVote(
removePaginationWrapper(data?.party?.votesConnection?.edges).find(
({ proposalId: pId }) => proposalId === pId
)
);
}
}, [
finalizedVote?.vote.value,
data?.party?.votesConnection?.edges,
finalizedVote,
proposalId,
pubKey,
]);
// If user vote changes update the vote state
@@ -95,8 +94,6 @@ export function useUserVote(
return {
voteState,
userVote,
voteDatetime: userVote?.vote?.datetime
? new Date(userVote.vote.datetime)
: null,
voteDatetime: userVote ? new Date(userVote.vote.datetime) : null,
};
}
@@ -7,8 +7,6 @@ import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from './__generated__/Proposal';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
@@ -22,36 +20,15 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const {
data: newMarketData,
loading: newMarketLoading,
error: newMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
},
});
useEffect(() => {
const interval = setInterval(refetch, 2000);
const interval = setInterval(refetch, 1000);
return () => clearInterval(interval);
}, [refetch]);
return (
<AsyncRenderer
loading={loading || newMarketLoading}
error={error || newMarketError}
data={newMarketData ? { newMarketData, data } : data}
>
<AsyncRenderer loading={loading} error={error} data={data}>
{data?.proposal ? (
<Proposal
proposal={data.proposal}
restData={restData}
newMarketData={newMarketData}
/>
<Proposal proposal={data.proposal} restData={restData} />
) : (
<ProposalNotFound />
)}
@@ -1,8 +1,10 @@
const dialogContent = 'dialog-content';
const nodeHealth = 'node-health';
const statusIncidentsLink = 'footer [data-testid=external-link]';
describe('home', { tags: '@regression' }, () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
@@ -74,14 +76,23 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/');
});
// 0006-NETW-002
// 0006-NETW-003
// 0006-NETW-011
it('switch to fairground network and check status & incidents link', () => {
// 0006-NETW-002
// 0006-NETW-003
cy.getByTestId('navigation')
.find('[data-testid="network-switcher"]')
.should('have.text', 'Custom')
.click();
cy.getByTestId('network-item').contains('Fairground testnet');
cy.getByTestId('network-item').contains('Fairground testnet').click();
cy.get('[aria-haspopup="menu"]').should('contain.text', 'Fairground');
cy.url().should('include', 'fairground.wtf');
cy.contains('Continue').click();
cy.get(statusIncidentsLink)
.children('span')
.should('have.text', 'Mainnet status & incidents');
cy.get(statusIncidentsLink)
.should('have.attr', 'href')
.and('contain', 'https://blog.vega.xyz/tagged/vega-incident-reports');
});
});
});
@@ -81,7 +81,6 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelector)
.first()
.find(colAverageEntryValuation)
@@ -166,7 +165,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
});
it('can see liquidity supplied', () => {
// 5002-LIQP-008
//// 5002-LIQP-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-supplied').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
@@ -238,7 +237,6 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
.find(colFee)
.should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelectorLiquidityActive)
.first()
.find(colAverageEntryValuation)
@@ -270,7 +268,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
});
it('renders liquidity inactive table correctly', () => {
// 5002-LIQP-012
//// 5002-LIQP-012
cy.getByTestId('Inactive').click();
cy.get(rowSelectorLiquidityInactive)
.first()
@@ -128,7 +128,6 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.wrap(btn).click();
});
}
cy.contains('Loading...').should('not.exist');
});
// 7001-COLL-010
it('sorting by asset', () => {
@@ -147,17 +146,12 @@ describe('accounts', { tags: '@smoke' }, () => {
it('sorting by total', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00002',
'1,000.00',
'1,000.01',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00001',
'1,000.00002',
'1,000.01',
];
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
@@ -194,24 +188,19 @@ describe('accounts', { tags: '@smoke' }, () => {
);
});
it('sorting by available', () => {
it('sorting by total', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00002',
'1,000.00',
'1,000.01',
'1,000.00',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00',
'1,000.00001',
'1,000.00002',
];
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'available',
'total',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
@@ -1,10 +1,6 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
accountsQuery,
amendGeneralAccountBalance,
amendMarginAccountBalance,
} from '@vegaprotocol/mock';
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
@@ -16,9 +12,8 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '0');
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -28,11 +23,6 @@ describe(
});
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
@@ -50,9 +40,8 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '1');
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -5,10 +5,6 @@ import {
toggleMarket,
} from '../support/deal-ticket';
const tooltipContent = 'tooltip-content';
const reduceOnly = 'reduce-only';
const postOnly = 'post-only';
describe('time in force validation', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
@@ -126,18 +122,13 @@ describe('time in force validation', { tags: '@smoke' }, () => {
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId(postOnly).should('be.disabled');
cy.getByTestId(reduceOnly).should('be.enabled');
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
});
@@ -153,33 +144,14 @@ describe('time in force validation', { tags: '@smoke' }, () => {
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId(postOnly).should('be.enabled');
cy.getByTestId(reduceOnly).should('be.disabled');
cy.getByTestId('post-only').should('be.enabled');
cy.getByTestId('reduce-only').should('be.disabled');
});
});
it(`can see explanation of what post only and reduce only is/does`, function () {
// 7003-SORD-058
cy.get('[for="post-only"]').should('have.text', 'Post only').realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.`
);
cy.get('[for="reduce-only"]')
.should('have.text', 'Reduce only')
.realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".`
);
});
});
});
@@ -1,79 +1,32 @@
const colHeader = '.ag-header-cell-text';
const colIdPrice = '[col-id=price]';
const colIdSize = '[col-id=size]';
const colIdCreatedAt = '[col-id=createdAt]';
const tradesTab = 'Trades';
const tradesTable = 'tab-trades';
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
});
describe('trades', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.getByTestId(tradesTab).click();
});
const colIdPrice = 'price';
const colIdSize = 'size';
const colIdCreatedAt = 'createdAt';
it('show trades', () => {
// 6005-THIS-001
// 6005-THIS-002
cy.getByTestId(tradesTab).should('be.visible');
cy.getByTestId(tradesTable).should('be.visible');
cy.getByTestId(tradesTable).should('not.be.empty');
});
it('renders trades', () => {
cy.getByTestId('Trades').click();
cy.getByTestId('tab-trades').should('be.visible');
it('show trades prices', () => {
// 6005-THIS-003
cy.get(`${colIdPrice} ${colHeader}`).first().should('have.text', 'Price');
cy.get(colIdPrice).each(($tradePrice) => {
cy.get(`[col-id=${colIdPrice}]`).each(($tradePrice) => {
cy.wrap($tradePrice).invoke('text').should('not.be.empty');
});
});
it('show trades sizes', () => {
// 6005-THIS-004
cy.get(`${colIdSize} ${colHeader}`).first().should('have.text', 'Size');
cy.get(colIdSize).each(($tradeSize) => {
cy.get(`[col-id=${colIdSize}]`).each(($tradeSize) => {
cy.wrap($tradeSize).invoke('text').should('not.be.empty');
});
});
it('show trades date and time', () => {
// 6005-THIS-005
cy.get(`${colIdCreatedAt} ${colHeader}`).should('have.text', 'Created at');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.get(colIdCreatedAt).each(($tradeDateTime, index) => {
cy.get(`[col-id=${colIdCreatedAt}]`).each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
}
});
});
it('trades are sorted descending by datetime', () => {
// 6005-THIS-006
const dateTimes: Date[] = [];
cy.get(colIdCreatedAt)
.each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
dateTimes.push(new Date($tradeDateTime.text()));
}
})
.then(() => {
expect(dateTimes).to.deep.equal(
dateTimes.sort((a, b) => b.getTime() - a.getTime())
);
});
});
it('copy price to deal ticket form', () => {
// 6005-THIS-007
cy.get(colIdPrice).last().click();
cy.getByTestId('order-price').should('have.value', '171.16898');
});
});
@@ -83,8 +83,6 @@ export const LiquidityContainer = ({
const assetDecimalPlaces =
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const quantum =
market?.tradableInstrument.instrument.product.settlementAsset.quantum || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
@@ -100,7 +98,6 @@ export const LiquidityContainer = ({
rowData={data}
symbol={symbol}
assetDecimalPlaces={assetDecimalPlaces}
quantum={quantum}
stakeToCcyVolume={stakeToCcyVolume}
overlayNoRowsTemplate={error ? error.message : t('No data')}
/>
@@ -49,7 +49,7 @@ const MarketBottomPanel = memo(
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'bottom' });
const { screenSize } = useScreenDimensions();
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
return 'xxxl' === screenSize ? (
<ResizableGrid
@@ -39,7 +39,7 @@ export const TradePanels = ({
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
@@ -22,7 +22,7 @@ export const DepositsContainer = () => {
<div className="h-full">
<div className="h-full relative">
<DepositsTable
rowData={data}
rowData={data || []}
ref={gridRef}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
@@ -34,7 +34,7 @@ export const Portfolio = () => {
}, [updateTitle]);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'h-full max-h-full flex flex-col';
return (
@@ -125,19 +125,14 @@ export const MarketLiquiditySupplied = ({
</KeyValueTableRow>
</KeyValueTable>
<br />
<div className="flex flex-col gap-2">
<Link
href={`/#/liquidity/${marketId}`}
data-testid="view-liquidity-link"
>
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
</div>
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
{showMessage && (
<p className="mt-4">
{t(
@@ -20,8 +20,20 @@ export const useMarketClickHandler = (replace = false) => {
);
};
export const useMarketLiquidityClickHandler = () => {
return useCallback((selectedId: string, metaKey?: boolean) => {
window.open(`/#/liquidity/${selectedId}`, metaKey ? '_blank' : '_self');
}, []);
export const useMarketLiquidityClickHandler = (replace = false) => {
const navigate = useNavigate();
const { marketId } = useParams();
const { pathname } = useLocation();
const isLiquidityPage = pathname.match(/^\/liquidity\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.LIQUIDITY](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else if (selectedId !== marketId || !isLiquidityPage) {
navigate(link, { replace });
}
},
[navigate, marketId, replace, isLiquidityPage]
);
};
@@ -72,6 +72,7 @@ export const AccountBreakdownDialog = memo(
onClose: () => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
console.log('render');
return (
<Dialog
size="medium"
+13 -47
View File
@@ -151,53 +151,19 @@ export const amendGeneralAccountBalance = (
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const marginAccount = accounts.party?.accountsConnection?.edges?.find(
(edge) => edge?.node.market?.id === marketId
);
if (marginAccount) {
const edges = accounts.party.accountsConnection.edges.map((edge) =>
edge?.node.asset.id === marginAccount.node.asset.id && !edge?.node.market
? { ...edge, node: { ...edge.node, balance } }
: edge
if (accounts.party?.accountsConnection?.edges) {
const marginAccount = accounts.party.accountsConnection.edges.find(
(edge) => edge?.node.market?.id === marketId
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
if (marginAccount) {
const generalAccount = accounts.party.accountsConnection.edges.find(
(edge) =>
edge?.node.asset.id === marginAccount.node.asset.id &&
!edge?.node.market
);
if (generalAccount) {
generalAccount.node.balance = balance;
}
}
}
return accounts;
};
export const amendMarginAccountBalance = (
accounts: AccountsQuery,
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const edges = accounts.party?.accountsConnection?.edges?.map((edge) =>
edge?.node.market?.id === marketId
? { ...edge, node: { ...edge?.node, balance } }
: edge
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
};
-8
View File
@@ -290,14 +290,6 @@ export const TransferFee = ({
decimals?: number;
}) => {
if (!feeFactor || !amount || !transferAmount || !fee) return null;
if (
isNaN(Number(feeFactor)) ||
isNaN(Number(amount)) ||
isNaN(Number(transferAmount)) ||
isNaN(Number(fee))
) {
return null;
}
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
+14 -22
View File
@@ -44,35 +44,27 @@ export const checkSorting = (
orderTabDesc: string[]
) => {
checkSortChange(orderTabDefault, column);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).last().click();
});
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabAsc, column);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabDesc, column);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
};
const checkSortChange = (tabsArr: string[], column: string) => {
cy.get('.ag-center-cols-container')
.last()
.within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
});
cy.get('.ag-center-cols-container').within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
});
});
});
};
type Edges = { node: unknown }[];
+4 -6
View File
@@ -24,13 +24,11 @@ export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
TField
>;
export type VegaValueGetterParams<TRow> = Omit<
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
ValueGetterParams,
'data' | 'node'
> & {
data?: TRow;
node: (Omit<RowNode, 'data'> & { data?: TRow }) | null;
};
TRow,
TField
>;
export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit<
RowHelper<ICellRendererParams, TRow, TField>,
@@ -1,6 +1,6 @@
import { formatRange, formatValue } from './range';
import { formatRange, formatValue } from './deal-ticket-fee-details';
describe('formatValue', () => {
describe('formatRange, formatValue', () => {
it.each([
{ v: 123000, d: 5, o: '1.23' },
{ v: 123000, d: 3, o: '123.00' },
@@ -21,12 +21,12 @@ describe('formatValue', () => {
{ v: 123000, d: 3, o: '123.00', q: '0.1' },
{ v: 123000, d: 1, o: '12,300.00', q: '0.1' },
{ v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' },
{ v: 123001, d: 2, o: '1,230.01', q: '100' },
{ v: 123001, d: 2, o: '1,230', q: '100' },
{ v: 123001, d: 2, o: '1,230.01', q: '0.1' },
{
v: '123456789123456789',
d: 10,
o: '12,345,678.91234568',
o: '12,345,678.9123457',
q: '0.00003846',
},
])(
@@ -35,16 +35,9 @@ describe('formatValue', () => {
expect(formatValue(v.toString(), d, q)).toStrictEqual(o);
}
);
});
describe('formatRange', () => {
it.each([
{
min: 123000,
max: 12300011111,
d: 5,
o: '1.23 - 123,000.11111',
q: '0.1',
},
{ min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' },
{
min: 123000,
max: 12300011111,
@@ -63,7 +56,7 @@ describe('formatRange', () => {
min: 123001000,
max: 12300011111,
d: 2,
o: '1,230,010.00 - 123,000,111.11',
o: '1,230,010 - 123,000,111',
q: '100',
},
])(
@@ -11,7 +11,11 @@ import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
isNumeric,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -27,6 +31,32 @@ import {
const emptyValue = '-';
export const formatValue = (
value: string | number | null | undefined,
formatDecimals: number,
quantum?: string
): string => {
if (!isNumeric(value)) return emptyValue;
if (!quantum) return addDecimalsFormatNumber(value, formatDecimals);
return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum);
};
export const formatRange = (
min: string | number | null | undefined,
max: string | number | null | undefined,
formatDecimals: number,
quantum?: string
) => {
const minFormatted = formatValue(min, formatDecimals, quantum);
const maxFormatted = formatValue(max, formatDecimals, quantum);
if (minFormatted !== maxFormatted) {
return `${minFormatted} - ${maxFormatted}`;
}
if (minFormatted !== emptyValue) {
return minFormatted;
}
return maxFormatted;
};
export interface DealTicketFeeDetailPros {
label: string;
value?: string | null | undefined;
@@ -113,7 +143,6 @@ export const DealTicketFeeDetails = ({
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
@@ -203,6 +232,7 @@ export const DealTicketFeeDetails = ({
}
let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateFormatted;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
@@ -228,10 +258,18 @@ export const DealTicketFeeDetails = ({
liquidationEstimateWorstCaseIncludingSellOrders
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
// The estimate order query API gives us the liquidation price in formatted by asset decimals.
// We need to calculate it with asset decimals, but display it with market decimals precision until the API changes.
liquidationPriceEstimate = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals
);
liquidationPriceEstimateFormatted = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
@@ -241,8 +279,7 @@ export const DealTicketFeeDetails = ({
: liquidationEstimateWorstCase
).toString(),
assetDecimals,
undefined,
market.decimalPlaces
quantum
);
}
@@ -251,16 +288,14 @@ export const DealTicketFeeDetails = ({
[]
);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<div>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
value={formatValue(notionalSize, assetDecimals)}
formattedValue={formatValue(notionalSize, assetDecimals, quantum)}
symbol={assetSymbol}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol)}
/>
<DealTicketFeeDetail
label={t('Fees')}
@@ -342,8 +377,8 @@ export const DealTicketFeeDetails = ({
<DealTicketFeeDetail
label={t('Liquidation price estimate')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
formattedValue={liquidationPriceEstimateFormatted}
symbol={assetSymbol}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
/>
{partyId && (
@@ -195,7 +195,7 @@ export const DealTicket = ({
}
const hasNoBalance =
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
!generalAccountBalance || !BigInt(generalAccountBalance);
if (hasNoBalance) {
setError('summary', {
message: SummaryValidationType.NoCollateral,
@@ -219,7 +219,6 @@ export const DealTicket = ({
marketState,
marketTradingMode,
generalAccountBalance,
marginAccountBalance,
pubKey,
setError,
clearErrors,
+2
View File
@@ -6,6 +6,7 @@ import {
truncateByChars,
isNumeric,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
@@ -24,6 +25,7 @@ export const DepositsTable = forwardRef<
return (
<AgGrid
ref={ref}
overlayNoRowsTemplate={t('No deposits')}
defaultColDef={{ resizable: true }}
style={{ width: '100%', height: '100%' }}
suppressCellFocus={true}
+112 -154
View File
@@ -1,22 +1,20 @@
import { forwardRef, useMemo } from 'react';
import { forwardRef } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
formatNumberPercentage,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import type {
VegaValueFormatterParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type {
ColDef,
ITooltipParams,
ValueFormatterParams,
} from 'ag-grid-community';
import { AgGridColumn } from 'ag-grid-react';
import type { ValueFormatterParams } from 'ag-grid-community';
import BigNumber from 'bignumber.js';
import type { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
import type { LiquidityProvisionData } from './liquidity-data-provider';
@@ -37,152 +35,21 @@ export interface LiquidityTableProps
symbol?: string;
assetDecimalPlaces?: number;
stakeToCcyVolume: string | null;
quantum?: string | number;
}
export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
(
{ symbol = '', assetDecimalPlaces, stakeToCcyVolume, quantum, ...props },
ref
) => {
const colDefs = useMemo(() => {
const assetDecimalsFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`;
};
const assetDecimalsQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
};
const stakeToCcyVolumeQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumberQuantum(
newValue,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const defs: ColDef[] = [
{
headerName: t('Party'),
field: 'party.id',
headerTooltip: t(
'The public key of the party making this commitment.'
),
},
{
headerName: t(`Commitment (${symbol})`),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t(`Share`),
field: 'equityLikeShare',
type: 'rightAligned',
headerTooltip: t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
),
valueFormatter: percentageFormatter,
},
{
headerName: t('Proposed fee'),
headerTooltip: t(
'The fee percentage (per trade) proposed by each liquidity provider.'
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
},
{
headerName: t('Market valuation at entry'),
field: 'averageEntryValuation',
type: 'rightAligned',
headerTooltip: t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
),
minWidth: 160,
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t('Obligation'),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_volume network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Supplied'),
field: 'balance',
type: 'rightAligned',
headerTooltip: t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
},
},
{
headerName: t('Created'),
headerTooltip: t(
'The date and time this liquidity provision was created.'
),
field: 'createdAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
{
headerName: t('Updated'),
headerTooltip: t(
'The date and time this liquidity provision was last updated.'
),
field: 'updatedAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
];
return defs;
}, [assetDecimalPlaces, quantum, stakeToCcyVolume, symbol]);
({ symbol = '', assetDecimalPlaces, stakeToCcyVolume, ...props }, ref) => {
const assetDecimalsFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0, 5)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0, 5)}`;
};
return (
<AgGrid
@@ -199,8 +66,99 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
}}
storeKey="liquidityProvisionTable"
{...props}
columnDefs={colDefs}
></AgGrid>
>
<AgGridColumn
headerName={t('Party')}
field="party.id"
headerTooltip={t(
'The public key of the party making this commitment.'
)}
/>
<AgGridColumn
headerName={t(`Commitment (${symbol})`)}
field="commitmentAmount"
type="rightAligned"
headerTooltip={t(
'The amount committed to the market by this liquidity provider.'
)}
valueFormatter={assetDecimalsFormatter}
/>
<AgGridColumn
headerName={t('Share')}
field="equityLikeShare"
type="rightAligned"
headerTooltip={t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
)}
valueFormatter={percentageFormatter}
/>
<AgGridColumn
headerName={t('Proposed fee')}
headerTooltip={t(
'The fee percentage (per trade) proposed by each liquidity provider.'
)}
field="fee"
type="rightAligned"
valueFormatter={percentageFormatter}
/>
<AgGridColumn
headerName={t('Market valuation at entry')}
field="averageEntryValuation"
type="rightAligned"
headerTooltip={t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
)}
minWidth={160}
valueFormatter={assetDecimalsFormatter}
/>
<AgGridColumn
headerName={t('Obligation')}
field="commitmentAmount"
type="rightAligned"
headerTooltip={t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_siskas network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
)}
valueFormatter={stakeToCcyVolumeFormatter}
/>
<AgGridColumn
headerName={t('Supplied')}
headerTooltip={t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
)}
field="balance"
type="rightAligned"
valueFormatter={stakeToCcyVolumeFormatter}
/>
<AgGridColumn
headerName={t('Status')}
headerTooltip={t('The current status of this liquidity provision.')}
field="status"
valueFormatter={({
value,
}: VegaValueFormatterParams<LiquidityProvisionData, 'status'>) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[value];
}}
/>
<AgGridColumn
headerName={t('Created')}
headerTooltip={t(
'The date and time this liquidity provision was created.'
)}
field="createdAt"
type="rightAligned"
valueFormatter={dateValueFormatter}
/>
<AgGridColumn
headerName={t('Updated')}
headerTooltip={t(
'The last time this liquidity provision was updated.'
)}
field="updatedAt"
type="rightAligned"
valueFormatter={dateValueFormatter}
/>
</AgGrid>
);
}
);
@@ -76,7 +76,9 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
@@ -100,7 +102,12 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
@@ -127,7 +134,9 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
@@ -1,60 +0,0 @@
import { useEstimatePositionQuery } from './__generated__/Positions';
import { formatRange } from '@vegaprotocol/utils';
export const LiquidationPrice = ({
marketId,
openVolume,
collateralAvailable,
decimalPlaces,
formatDecimals,
}: {
marketId: string;
openVolume: string;
collateralAvailable: string;
decimalPlaces: number;
formatDecimals: number;
}) => {
const { data: currentData, previousData } = useEstimatePositionQuery({
variables: {
marketId,
openVolume,
collateralAvailable,
},
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
);
}
return <span data-testid="liquidation-price">{value}</span>;
};
@@ -34,7 +34,6 @@ export interface Position {
averageEntryPrice: string;
currentLeverage: number | undefined;
decimals: number;
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
@@ -74,7 +73,6 @@ export const getMetrics = (
decimals,
id: assetId,
symbol: assetSymbol,
quantum,
} = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find(
(account) =>
@@ -116,7 +114,6 @@ export const getMetrics = (
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals,
quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
@@ -7,12 +7,6 @@ import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
jest.mock('./liquidation-price', () => ({
LiquidationPrice: () => (
<span data-testid="liquidation-price">liquidation price</span>
),
}));
const singleRow: Position = {
partyId: 'partyId',
assetId: 'asset-id',
@@ -20,7 +14,6 @@ const singleRow: Position = {
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2,
quantum: '0.1',
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
@@ -55,7 +48,7 @@ it('render correct columns', async () => {
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(12);
expect(headers).toHaveLength(11);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
@@ -63,7 +56,6 @@ it('render correct columns', async () => {
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
@@ -151,20 +143,12 @@ it('displays mark price', async () => {
expect(cells[3].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
expect(cells[6].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
@@ -172,7 +156,7 @@ it('displays allocated margin', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
const cell = cells[7];
expect(cell.textContent).toEqual('123,456.00');
});
@@ -181,7 +165,7 @@ it('displays realised and unrealised PNL', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[10].textContent).toEqual('4.56');
expect(cells[9].textContent).toEqual('4.56');
});
it('displays close button', async () => {
@@ -198,7 +182,7 @@ it('displays close button', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
expect(cells[11].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
@@ -214,7 +198,7 @@ it('do not display close button if openVolume is zero', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
expect(cells[11].textContent).toEqual('');
});
describe('PNLCell', () => {
+325 -336
View File
@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { forwardRef, useMemo } from 'react';
import { forwardRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { ColDef } from 'ag-grid-community';
import type { CellRendererSelectorResult } from 'ag-grid-community';
import type {
VegaValueFormatterParams,
VegaValueGetterParams,
@@ -33,6 +33,7 @@ import {
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
@@ -42,7 +43,6 @@ import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { LiquidationPrice } from './liquidation-price';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -121,344 +121,333 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
MarketNameCell,
}}
{...props}
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
? {
headerName: t('Vega key'),
field: 'partyId',
valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)
?.name) ||
data?.partyId,
minWidth: 190,
}
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
minWidth: 190,
},
{
headerName: t('Notional'),
headerTooltip: t('Mark price x open volume.'),
field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(
data.notional,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 80,
},
{
headerName: t('Open volume'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
},
cellRenderer: OpenVolumeCell,
minWidth: 100,
},
{
headerName: t('Mark price'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(
data.markPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
>
{multipleKeys ? (
<AgGridColumn
headerName={t('Vega key')}
field="partyId"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'partyId'>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)?.name) ||
data?.partyId
}
minWidth={190}
/>
) : null}
<AgGridColumn
headerName={t('Market')}
field="marketName"
cellRenderer="MarketNameCell"
cellRendererParams={{ idPath: 'marketId', onMarketClick }}
minWidth={190}
/>
<AgGridColumn
headerName={t('Notional')}
headerTooltip={t('Mark price x open volume.')}
field="notional"
type="rightAligned"
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'notional'>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/>
}}
minWidth={80}
/>
<AgGridColumn
headerName={t('Open volume')}
field="openVolume"
type="rightAligned"
cellClass="font-mono text-right"
cellClassRules={signedNumberCssClassRules}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'openVolume'>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>):
| string
| undefined => {
return data?.openVolume === undefined
? undefined
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
},
},
{
headerName: t('Settlement asset'),
field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
},
},
{
headerName: t('Entry price'),
field: 'averageEntryPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
}}
cellRenderer={OpenVolumeCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Mark price')}
field="markPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'markPrice'>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return undefined;
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="assetSymbol"
colId="asset"
minWidth={100}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(data.assetId, e.target as HTMLElement);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
}}
/>
<AgGridColumn
headerName={t('Entry price')}
field="averageEntryPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'averageEntryPrice'>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? ''
: formatNumber(value.toString(), 1),
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Margin allocated'),
field: 'marginAccountBalance',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.decimals
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
minWidth: 100,
},
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
},
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.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Unrealised PNL'),
field: 'unrealisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Updated'),
field: 'updatedAt',
type: 'rightAligned',
filter: DateRangeFilter,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return '';
}
return getDateTimeFormat().format(new Date(value));
},
minWidth: 150,
},
onClose && !isReadOnly
? {
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
},
minWidth: 90,
maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/>
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Leverage')}
field="currentLeverage"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? undefined
: formatNumber(value.toString(), 1)
}
minWidth={100}
/>
)}
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Margin allocated')}
field="marginAccountBalance"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data
? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
}}
minWidth={100}
/>
)}
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
}}
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.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
field="unrealisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? undefined
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals)
}
headerTooltip={t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Updated')}
field="updatedAt"
type="rightAligned"
filter={DateRangeFilter}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return value;
}
return getDateTimeFormat().format(new Date(value));
}}
minWidth={150}
/>
{onClose && !isReadOnly ? (
<AgGridColumn
{...COL_DEFS.actions}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
}}
minWidth={90}
maxWidth={90}
/>
) : null}
</AgGrid>
);
}
);
@@ -1,2 +1 @@
export * from './use-vote-submit';
export * from './__generated__/VoteSubsciption';
@@ -5,16 +5,15 @@ import { useVoteEvent } from './use-vote-event';
import type { VoteValue } from '@vegaprotocol/types';
import type { VoteEventFieldsFragment } from './__generated__/VoteSubsciption';
export type FinalizedVote = VoteEventFieldsFragment & { pubKey: string };
export type FinalizedVote = VoteEventFieldsFragment;
export const useVoteSubmit = () => {
const { pubKey } = useVegaWallet();
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
const waitForVoteEvent = useVoteEvent(transaction);
const [finalizedVote, setFinalizedVote] = useState<FinalizedVote | null>(
null
);
const [finalizedVote, setFinalizedVote] =
useState<VoteEventFieldsFragment | null>(null);
const submit = useCallback(
async (voteValue: VoteValue, proposalId: string | null) => {
@@ -34,7 +33,7 @@ export const useVoteSubmit = () => {
if (res) {
waitForVoteEvent(proposalId, pubKey, (v) => {
setFinalizedVote({ ...v, pubKey });
setFinalizedVote(v);
setComplete();
});
}
+8 -8
View File
@@ -53,10 +53,10 @@ export const tradesUpdateSubscription = (
const trades: TradeFieldsFragment[] = [
{
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
@@ -77,10 +77,10 @@ const trades: TradeFieldsFragment[] = [
__typename: 'Trade',
},
{
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
-1
View File
@@ -1,5 +1,4 @@
export * from './date';
export * from './number';
export * from './range';
export * from './size';
export * from './strings';
+73 -80
View File
@@ -29,21 +29,14 @@ describe('number utils', () => {
{ v: new BigNumber(123000), d: 3, o: '123.00', q: 0.1 },
{ v: new BigNumber(123000), d: 1, o: '12,300.00', q: 0.1 },
{ v: new BigNumber(123001000), d: 2, o: '1,230,010.00', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 1 },
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
o: '12,345,678.9123457',
q: '0.00003846',
},
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
q: '1',
},
])(
'formats with addDecimalsFormatNumberQuantum given number correctly',
({ v, d, o, q }) => {
@@ -77,80 +70,80 @@ describe('number utils', () => {
])('formats given number correctly', ({ v, d, o }) => {
expect(formatNumberPercentage(v, d)).toStrictEqual(o);
});
});
describe('toNumberParts', () => {
it.each([
{ v: null, d: 3, o: ['0', '000'] },
{ v: undefined, d: 3, o: ['0', '000'] },
{ v: new BigNumber(123), d: 3, o: ['123', '00'] },
{ v: new BigNumber(123.123), d: 3, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 6, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 0, o: ['123', ''] },
{ v: new BigNumber(123), d: undefined, o: ['123', '00'] },
{
v: new BigNumber(30000),
d: undefined,
o: ['30,000', '00'],
},
])('returns correct tuple given the different arguments', ({ v, d, o }) => {
expect(toNumberParts(v, d)).toStrictEqual(o);
});
describe('toNumberParts', () => {
it.each([
{ v: null, d: 3, o: ['0', '000'] },
{ v: undefined, d: 3, o: ['0', '000'] },
{ v: new BigNumber(123), d: 3, o: ['123', '00'] },
{ v: new BigNumber(123.123), d: 3, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 6, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 0, o: ['123', ''] },
{ v: new BigNumber(123), d: undefined, o: ['123', '00'] },
{
v: new BigNumber(30000),
d: undefined,
o: ['30,000', '00'],
},
])('returns correct tuple given the different arguments', ({ v, d, o }) => {
expect(toNumberParts(v, d)).toStrictEqual(o);
});
});
describe('isNumeric', () => {
it.each([
{ i: null, o: false },
{ i: undefined, o: false },
{ i: 1, o: true },
{ i: '1', o: true },
{ i: '-1', o: true },
{ i: 0.1, o: true },
{ i: '.1', o: true },
{ i: '-.1', o: true },
{ i: 123, o: true },
{ i: -123, o: true },
{ i: '123', o: true },
{ i: '123.01', o: true },
{ i: '-123.01', o: true },
{ i: '--123.01', o: false },
{ i: '123.', o: false },
{ i: '123.1.1', o: false },
{ i: BigInt(123), o: true },
{ i: BigInt(-1), o: true },
{ i: new BigNumber(123), o: true },
{ i: new BigNumber(123.123), o: true },
{ i: new BigNumber(123.123).toString(), o: true },
{ i: new BigNumber(123), o: true },
{ i: Infinity, o: false },
{ i: NaN, o: false },
])(
'returns correct results',
({
i,
o,
}: {
i: number | string | undefined | null | BigNumber | bigint;
o: boolean;
}) => {
expect(isNumeric(i)).toStrictEqual(o);
}
);
});
describe('isNumeric', () => {
it.each([
{ i: null, o: false },
{ i: undefined, o: false },
{ i: 1, o: true },
{ i: '1', o: true },
{ i: '-1', o: true },
{ i: 0.1, o: true },
{ i: '.1', o: true },
{ i: '-.1', o: true },
{ i: 123, o: true },
{ i: -123, o: true },
{ i: '123', o: true },
{ i: '123.01', o: true },
{ i: '-123.01', o: true },
{ i: '--123.01', o: false },
{ i: '123.', o: false },
{ i: '123.1.1', o: false },
{ i: BigInt(123), o: true },
{ i: BigInt(-1), o: true },
{ i: new BigNumber(123), o: true },
{ i: new BigNumber(123.123), o: true },
{ i: new BigNumber(123.123).toString(), o: true },
{ i: new BigNumber(123), o: true },
{ i: Infinity, o: false },
{ i: NaN, o: false },
])(
'returns correct results',
({
i,
o,
}: {
i: number | string | undefined | null | BigNumber | bigint;
o: boolean;
}) => {
expect(isNumeric(i)).toStrictEqual(o);
}
);
});
describe('toDecimal', () => {
it.each([
{ v: 0, o: '1' },
{ v: 1, o: '0.1' },
{ v: 2, o: '0.01' },
{ v: 3, o: '0.001' },
{ v: 4, o: '0.0001' },
{ v: 5, o: '0.00001' },
{ v: 6, o: '0.000001' },
{ v: 7, o: '0.0000001' },
{ v: 8, o: '0.00000001' },
{ v: 9, o: '0.000000001' },
])('formats with toNumber given number correctly', ({ v, o }) => {
expect(toDecimal(v)).toStrictEqual(o);
});
describe('toDecimal', () => {
it.each([
{ v: 0, o: '1' },
{ v: 1, o: '0.1' },
{ v: 2, o: '0.01' },
{ v: 3, o: '0.001' },
{ v: 4, o: '0.0001' },
{ v: 5, o: '0.00001' },
{ v: 6, o: '0.000001' },
{ v: 7, o: '0.0000001' },
{ v: 8, o: '0.00000001' },
{ v: 9, o: '0.000000001' },
])('formats with toNumber given number correctly', ({ v, o }) => {
expect(toDecimal(v)).toStrictEqual(o);
});
});
+1 -2
View File
@@ -98,8 +98,7 @@ export const addDecimalsFormatNumberQuantum = (
if (isNaN(Number(quantum))) {
return addDecimalsFormatNumber(rawValue, decimalPlaces);
}
const quantumValue = addDecimal(quantum, decimalPlaces);
const numberDP = Math.max(0, Math.log10(100 / Number(quantumValue)));
const numberDP = Math.max(0, Math.log10(100 / Number(quantum)));
return addDecimalsFormatNumber(rawValue, decimalPlaces, Math.ceil(numberDP));
};
-38
View File
@@ -1,38 +0,0 @@
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
isNumeric,
} from './number';
export const formatValue = (
value: string | number | null | undefined,
decimalPlaces: number,
quantum?: string,
formatDecimals?: number,
emptyValue = '-'
): string => {
if (!isNumeric(value)) return emptyValue;
if (!quantum) {
return addDecimalsFormatNumber(value, decimalPlaces, formatDecimals);
}
return addDecimalsFormatNumberQuantum(value, decimalPlaces, quantum);
};
export const formatRange = (
min: string | number | null | undefined,
max: string | number | null | undefined,
decimalPlaces: number,
quantum?: string,
formatDecimals?: number,
emptyValue = '-'
) => {
const minFormatted = formatValue(min, decimalPlaces, quantum, formatDecimals);
const maxFormatted = formatValue(max, decimalPlaces, quantum, formatDecimals);
if (minFormatted !== maxFormatted) {
return `${minFormatted} - ${maxFormatted}`;
}
if (minFormatted !== emptyValue) {
return minFormatted;
}
return maxFormatted;
};