Compare commits

..
Author SHA1 Message Date
Matthew Russell 8c5eb0f3eb chore: add try catch in case stored block height is not valid json 2023-08-16 15:49:55 +01:00
Matthew Russell d6a8134f97 chore: change non const to normal camel case 2023-08-16 15:45:07 +01:00
asiaznik ffdf4f85ae fix: string error, links 2023-08-10 13:44:38 +02:00
asiaznik 0d1a8a5d3f chore: env 2023-08-10 13:44:38 +02:00
asiaznik 49d4046cc1 feat(proposals): upgrade in progress notification
chore: types

chore: remove appVersion from query

chore: block rising hook, build error fix

chore: block rising hook, build error fix

chore: foolproofing, added banner to governance

chore: upgrade banners in explorer
2023-08-10 13:44:17 +02:00
Maciekandasiaznik 069b57d4ae feat(wallet): 4370 new connect wallet modal (#4492)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-08-10 13:03:53 +02:00
Mikołaj Młodzikowski 6894fe1264 fix(ci): hardcode env name 2023-08-10 12:53:35 +02:00
Edd 93e4b5fdb9 feat(explorer): successor markets (#4522) 2023-08-10 09:51:44 +01:00
Maciek 1566fc3984 chore: fulfill spec to 4323 (#4520) 2023-08-10 09:20:26 +02:00
Ben 39962a0246 chore(trading): update console-test runners (#4513) 2023-08-09 14:18:24 +01:00
Sam KeenandJoe 8d31510d5e feat(governance,markets,ui-toolkit): highlight successor market changes (#4449)
Co-authored-by: Joe <joe@vega.xyz>
2023-08-09 14:05:37 +01:00
Bartłomiej Głownia 26d5a67604 feat(trading): use custom toast content for stop order related transactions (#4476) 2023-08-09 14:47:10 +02:00
Bartłomiej Głownia 9cc8f5a377 feat(trading): use instrument code instead of name in positions, fills and accounts breakdown tables (#4478) 2023-08-09 12:12:07 +02:00
Matthew Russell 2e11cf4dfa fix(trading): fix persitance of resizable grid top section (#4508) 2023-08-09 11:04:32 +01:00
Bartłomiej Głownia 9e4ba9f275 feat(trading): merge deal ticket stores (#4494) 2023-08-09 11:54:16 +02:00
m.ray aac0c25d09 fix(trading): positions tooltips update (#4504) 2023-08-08 13:27:17 +01:00
Art d20c2a08ae chore(specs): succession line specs (#4505) 2023-08-08 11:23:28 +01:00
daro-maj 4e6ba5fe3d test(trading): update cypress tests (#4503) 2023-08-08 10:36:10 +02:00
Art 276d1d7c7b feat(governance): view as button (#4501) 2023-08-08 09:50:19 +02:00
m.ray 4990c5808d fix(trading): order iceberg fills (#4502) 2023-08-07 16:07:24 +00:00
145 changed files with 3540 additions and 2740 deletions
+3 -3
View File
@@ -10,8 +10,8 @@ on:
jobs:
run-tests:
name: run-tests
runs-on: ubuntu-22.04
timeout-minutes: 25
runs-on: 8-cores
timeout-minutes: 20
steps:
#----------------------------------------------
# check-out frontend-monorepo
@@ -125,7 +125,7 @@ jobs:
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest --numprocesses auto
run: poetry run pytest -s --numprocesses auto
- name: Check files
run: |
ls -al .
+3 -2
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_ENV=CUSTOM
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
@@ -8,3 +6,6 @@ NX_VEGA_EXPLORER_URL=/
# App flags
NX_EXPLORER_TXS_LIST=0
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
+3 -2
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_VEGA_ENV=DEVNET
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
@@ -10,3 +8,6 @@ NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_EXPLORER_URL=/
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
+3 -2
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/6242427
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_URL=https://api.vega.community/graphql
@@ -11,3 +9,6 @@ NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+4 -3
View File
@@ -1,6 +1,4 @@
# App configuration variables
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
@@ -10,4 +8,7 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks/
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+3 -2
View File
@@ -1,7 +1,5 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
@@ -11,3 +9,6 @@ NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+3 -2
View File
@@ -7,9 +7,10 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
NX_VEGA_GOVERNANCE_URL=https://governance.validators-testnet.vega.rocks
NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks/
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
@@ -1,6 +1,9 @@
import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets';
import {
PriceMonitoringBoundsInfoPanel,
SuccessionLineInfoPanel,
} from '@vegaprotocol/markets';
import {
LiquidityInfoPanel,
LiquidityMonitoringParametersInfoPanel,
@@ -103,6 +106,8 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
<OracleInfoPanel market={market} type="settlementData" />
</>
)}
<h2 className={`${headerClassName} mb-4`}>{t('Succession line')}</h2>
<SuccessionLineInfoPanel market={market} />
</div>
);
};
@@ -10,6 +10,8 @@ import Hash from '../../links/hash';
import { t } from '@vegaprotocol/i18n';
import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new';
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
import { MarketLink } from '../../links';
import { formatNumber } from '@vegaprotocol/utils';
export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
@@ -53,7 +55,11 @@ export function proposalTypeLabel(terms?: ProposalTerms): string {
} else if (has(terms, 'updateAsset')) {
return t('Update asset proposal');
} else if (has(terms, 'newMarket')) {
return t('New market proposal');
if (terms?.newMarket?.changes?.successor) {
return t('Successor market proposal');
} else {
return t('New market proposal');
}
} else if (has(terms, 'updateMarket')) {
return t('Update market proposal');
} else if (has(terms, 'updateNetworkParameter')) {
@@ -85,6 +91,13 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
}
const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset;
const isSuccessorMarketProposal =
proposal?.terms?.newMarket?.changes?.successor;
const parentMarketId =
isSuccessorMarketProposal &&
proposal?.terms.newMarket?.changes?.successor?.parentMarketId;
const insurancePoolFraction =
proposal?.terms?.newMarket?.changes?.successor?.insurancePoolFraction;
// This component is not rendered if no bundle is required
const SignatureBundleComponent = proposal.terms?.newAsset
@@ -111,6 +124,30 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
<Hash text={deterministicId} />
</TableCell>
</TableRow>
{isSuccessorMarketProposal ? (
<>
{parentMarketId ? (
<TableRow modifier="bordered">
<TableCell>{t('Previous market')}</TableCell>
<TableCell>
<MarketLink
id={
proposal.terms?.newMarket.changes.successor.parentMarketId
}
/>
</TableCell>
</TableRow>
) : null}
{insurancePoolFraction ? (
<TableRow modifier="bordered">
<TableCell>{t('Insurance pool fraction')}</TableCell>
<TableCell>
{formatNumber(Number(insurancePoolFraction) * 100, 0)}%
</TableCell>
</TableRow>
) : null}
</>
) : null}
</TableWithTbody>
<ProposalSummary
id={deterministicId}
@@ -78,7 +78,11 @@ export function getLabelForProposal(
} else if (proposal.terms?.updateAsset) {
return t('Proposal: Update asset');
} else if (proposal.terms?.newMarket) {
return t('Proposal: New market');
if (proposal.terms?.newMarket.changes?.successor) {
return t('Proposal: Successor market');
} else {
return t('Proposal: New market');
}
} else if (proposal.terms?.updateMarket) {
return t('Proposal: Update market');
} else if (proposal.terms?.updateNetworkParameter) {
+9
View File
@@ -22,6 +22,11 @@ import { Footer } from '../components/footer/footer';
import { Header } from '../components/header';
import { Routes } from './route-names';
import { useExplorerNodeNamesLazyQuery } from './validators/__generated__/NodeNames';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
@@ -62,6 +67,10 @@ export const Layout = () => {
/>
)}
<Header />
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
</div>
<div className={fixedWidthClasses}>
<main className="p-4">
@@ -730,6 +730,7 @@ context(
.getByTestId('key-value-table-row')
.contains(heading)
.parent()
.parent()
.siblings();
}
}
@@ -25,6 +25,9 @@ import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
@@ -55,6 +58,10 @@ context(
function () {
// 1002-STKE-002, 1002-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/');
ethereumWalletConnect();
cy.connectVegaWallet();
@@ -65,6 +72,10 @@ context(
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.clearLocalStorage();
turnTelemetryOff();
// Go to homepage to allow wallet teardown without epoch timer refreshing page
@@ -6,6 +6,8 @@ import {
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { depositAsset } from '../../support/wallet-functions';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const withdraw = 'withdraw';
const withdrawalForm = 'withdraw-form';
@@ -42,12 +44,20 @@ context(
{ tags: '@slow' },
function () {
before('visit withdrawals and connect vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/');
ethereumWalletConnect();
depositAsset(usdcEthAddress, '1000', 5);
});
beforeEach('Navigate to withdrawal page', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.clearLocalStorage();
turnTelemetryOff();
cy.reload();
@@ -11,6 +11,7 @@ import {
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
import { previousEpochData } from '../../fixtures/mocks/previous-epoch';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const guideLink = 'staking-guide-link';
const validatorTitle = 'validator-node-title';
@@ -33,11 +34,22 @@ const overstakedPenaltyToolTip = 'overstaked-penalty-tooltip';
const multisigPenaltyToolTip = 'multisig-error-tooltip';
const epochCountDown = 'epoch-countdown';
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
const txTimeout = Cypress.env('txTimeout');
context('Validators Page - verify elements on page', function () {
before('navigate to validators page', function () {
before('navigate to validators page', () => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/validators');
});
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
it('Should have validators tab highlighted', function () {
@@ -177,6 +189,11 @@ context('Validators Page - verify elements on page', function () {
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/validators');
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
@@ -263,7 +280,7 @@ context('Validators Page - verify elements on page', function () {
cy.getByTestId(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
cy.get(nextEpochInfo, txTimeout).should('contain.text', 'Next epoch');
});
});
}
@@ -4,6 +4,8 @@ import {
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown,
} from '../../support/wallet-functions';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
@@ -14,10 +16,6 @@ const dialogHeader = 'dialog-title';
const walletDialogHeader = 'wallet-dialog-title';
const connectorsList = 'connectors-list';
const dialogCloseBtn = 'dialog-close';
const restConnectorForm = 'rest-connector-form';
const restWallet = '#wallet';
const restPassphrase = '#passphrase';
const restConnectBtn = '[type="submit"]';
const accountNo = 'vega-account-truncated';
const currencyTitle = 'currency-title';
const currencyValue = 'currency-value';
@@ -69,7 +67,7 @@ context(
cy.get(dialog).within(() => {
cy.getByTestId(walletDialogHeader)
.should('be.visible')
.and('have.text', 'Connect');
.and('have.text', 'Get a Vega wallet');
});
});
@@ -77,10 +75,7 @@ context(
cy.getByTestId(connectorsList).within(() => {
cy.getByTestId('connector-jsonRpc')
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
cy.getByTestId('connector-rest')
.should('be.visible')
.and('have.text', 'Hosted Fairground wallet');
.and('have.text', 'Use the Desktop App/CLI');
});
});
@@ -91,48 +86,14 @@ context(
});
});
describe('when rest connector form opened', function () {
before('click hosted wallet app button', function () {
cy.getByTestId(connectorsList).within(() => {
cy.getByTestId('connector-rest').click();
});
});
// 0002-WCON-002
it('should have wallet field visible', function () {
cy.getByTestId(restConnectorForm).within(() => {
cy.get(restWallet).should('be.visible');
});
});
it('should have password field visible', function () {
cy.getByTestId(restConnectorForm).within(() => {
cy.get(restPassphrase).should('be.visible');
});
});
it('should have connect button visible', function () {
cy.getByTestId(restConnectorForm).within(() => {
cy.get(restConnectBtn)
.should('be.visible')
.and('have.text', 'Connect');
});
});
it('should have close button visible', function () {
cy.get(dialog).within(() => {
cy.getByTestId(dialogCloseBtn).should('be.visible');
});
});
after('close dialog', function () {
cy.getByTestId(dialogCloseBtn).click().should('not.exist');
});
});
describe('when vega wallet connected', function () {
before('connect vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/');
cy.wait('@ChainId');
cy.connectVegaWallet();
vegaWalletTeardown();
});
@@ -315,6 +276,10 @@ context(
];
before('faucet assets to connected vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
for (const { id, amount } of assets) {
vegaWalletFaucetAssetsWithoutCheck(id, amount, vegaWalletPublicKey);
}
+4 -1
View File
@@ -21,9 +21,12 @@ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
#Test configuration variables
CYPRESS_FAIRGROUND=false
LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=true
@@ -6,6 +6,7 @@ import { Nav } from '../nav';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
@@ -49,6 +50,7 @@ const NotificationsContainer = () => {
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : null}
@@ -1,4 +1,8 @@
import { VegaConnectDialog, VegaManageDialog } from '@vegaprotocol/wallet';
import {
VegaConnectDialog,
VegaManageDialog,
ViewAsDialog,
} from '@vegaprotocol/wallet';
import {
AppStateActionType,
useAppState,
@@ -25,6 +29,8 @@ export const VegaWalletDialogs = () => {
})
}
/>
<ViewAsDialog connector={Connectors.view} />
</>
);
};
@@ -1,17 +0,0 @@
import { Link } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { ExternalLinks } from '@vegaprotocol/environment';
export const DownloadWalletPrompt = () => {
const { t } = useTranslation();
return (
<>
<h3 className="mt-4 mb-2">{t('getWallet')}</h3>
<p>
<Link className="text-neutral-500" href={ExternalLinks.VEGA_WALLET_URL}>
{t('getWalletLink')}
</Link>
</p>
</>
);
};
@@ -0,0 +1,26 @@
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { ExternalLinks } from '@vegaprotocol/environment';
import { useViewAsDialog } from '@vegaprotocol/wallet';
export const VegaWalletPrompt = () => {
const { t } = useTranslation();
const setViewAsDialog = useViewAsDialog((state) => state.setOpen);
return (
<>
<h3 className="mt-4 mb-2">{t('getWallet')}</h3>
<div className="flex flex-row gap-4">
<Link className="text-neutral-500" href={ExternalLinks.VEGA_WALLET_URL}>
{t('getWalletLink')}
</Link>
<ButtonLink
className="text-neutral-500"
data-testid="view-as-user"
onClick={() => setViewAsDialog(true)}
>
{t('viewAsParty')}
</ButtonLink>
</div>
</>
);
};
@@ -20,7 +20,7 @@ import {
WalletCardHeader,
WalletCardRow,
} from '../wallet-card';
import { DownloadWalletPrompt } from './download-wallet-prompt';
import { VegaWalletPrompt } from './vega-wallet-prompt';
import { usePollForDelegations } from './hooks';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
@@ -85,7 +85,7 @@ const VegaWalletNotConnected = () => {
>
{t('connectVegaWalletToUseAssociated')}
</Button>
<DownloadWalletPrompt />
<VegaWalletPrompt />
</>
);
};
@@ -855,5 +855,6 @@
"Estimated time to upgrade": "Estimated time to upgrade",
"Upgraded at": "Upgraded at",
"dataIsIdentical": "Data is identical",
"updatesToMarket": "Updates to market"
"updatesToMarket": "Updates to market",
"viewAsParty": "View as party"
}
@@ -1,5 +1,4 @@
import {
RestConnector,
JsonRpcConnector,
ViewConnector,
InjectedConnector,
@@ -8,13 +7,11 @@ import {
const urlParams = new URLSearchParams(window.location.search);
export const injected = new InjectedConnector();
export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const Connectors = {
injected,
rest,
jsonRpc,
view,
};
@@ -45,8 +45,10 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
export const ProposalMarketData = ({
marketData,
parentMarketData,
}: {
marketData: MarketInfoWithData;
parentMarketData?: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
@@ -58,8 +60,21 @@ export const ProposalMarketData = ({
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const parentSettlementData =
parentMarketData?.tradableInstrument.instrument?.product
?.dataSourceSpecForSettlementData?.data;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const parentTerminationData =
parentMarketData?.tradableInstrument.instrument?.product
?.dataSourceSpecForTradingTermination?.data;
const isParentSettlementDataEqual =
parentSettlementData !== undefined &&
isEqual(settlementData, parentSettlementData);
const isParentTerminationDataEqual =
parentTerminationData !== undefined &&
isEqual(terminationData, parentTerminationData);
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
@@ -97,12 +112,22 @@ export const ProposalMarketData = ({
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
content={
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
content={
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{isEqual(
getSigners(settlementData),
@@ -115,6 +140,11 @@ export const ProposalMarketData = ({
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
@@ -127,6 +157,11 @@ export const ProposalMarketData = ({
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
@@ -135,11 +170,21 @@ export const ProposalMarketData = ({
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual
? undefined
: parentMarketData
}
/>
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
@@ -148,22 +193,42 @@ export const ProposalMarketData = ({
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
content={
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
content={
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
content={
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
content={
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
@@ -174,6 +239,7 @@ export const ProposalMarketData = ({
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
parentMarket={parentMarketData}
triggerIndex={triggerIndex}
/>
}
@@ -183,13 +249,21 @@ export const ProposalMarketData = ({
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
content={
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
</Accordion>
</div>
@@ -32,6 +32,7 @@ export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
newMarketData?: MarketInfoWithData | null;
parentMarketData?: MarketInfoWithData | null;
assetData?: AssetQuery | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
@@ -46,6 +47,7 @@ export const Proposal = ({
networkParams,
restData,
newMarketData,
parentMarketData,
assetData,
originalMarketProposalRestData,
mostRecentlyEnactedAssociatedMarketProposal,
@@ -157,7 +159,10 @@ export const Proposal = ({
{newMarketData && (
<div className="mb-4">
<ProposalMarketData marketData={newMarketData} />
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
</div>
)}
@@ -14,6 +14,9 @@ import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { FLAGS } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
export const ProposalContainer = () => {
const [
@@ -54,6 +57,10 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const successor = useSuccessorMarketProposalDetails(params.proposalId);
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
const {
state: {
data: originalMarketProposalRestData,
@@ -96,6 +103,36 @@ export const ProposalContainer = () => {
},
});
const {
data: parentMarketId,
loading: parentMarketIdLoading,
error: parentMarketIdError,
} = useParentMarketIdQuery({
variables: {
marketId: newMarketData?.data?.market?.id || '',
},
skip:
!FLAGS.SUCCESSOR_MARKETS ||
!isSuccessor ||
!newMarketData?.data?.market?.id,
});
const {
data: parentMarketData,
loading: parentMarketLoading,
error: parentMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: parentMarketId?.market?.parentMarketID || '',
skip:
!FLAGS.SUCCESSOR_MARKETS ||
!isSuccessor ||
!parentMarketId?.market?.parentMarketID,
},
});
const {
data: assetData,
loading: assetLoading,
@@ -160,6 +197,8 @@ export const ProposalContainer = () => {
newMarketLoading ||
assetLoading ||
networkParamsLoading ||
parentMarketIdLoading ||
parentMarketLoading ||
(restLoading ? (restLoading as boolean) : false) ||
(originalMarketProposalRestLoading
? (originalMarketProposalRestLoading as boolean)
@@ -172,15 +211,18 @@ export const ProposalContainer = () => {
error ||
newMarketError ||
assetError ||
networkParamsError ||
parentMarketIdError ||
parentMarketError ||
restError ||
originalMarketProposalRestError ||
previouslyEnactedMarketProposalsRestError ||
networkParamsError
previouslyEnactedMarketProposalsRestError
}
data={{
...data,
...networkParams,
...(newMarketData ? { newMarketData } : {}),
...(parentMarketData ? { parentMarketData } : {}),
...(assetData ? { assetData } : {}),
...(restData ? { restData } : {}),
...(originalMarketProposalRestData
@@ -197,6 +239,7 @@ export const ProposalContainer = () => {
networkParams={networkParams}
restData={restData}
newMarketData={newMarketData}
parentMarketData={parentMarketData}
assetData={assetData}
originalMarketProposalRestData={originalMarketProposalRestData}
mostRecentlyEnactedAssociatedMarketProposal={
-1
View File
@@ -26,7 +26,6 @@ module.exports = defineConfig({
requestTimeout: 20000,
retries: 1,
testIsolation: false,
experimentalMemoryManagement: true,
},
env: {
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
@@ -104,11 +104,11 @@ describe('deposit actions', { tags: '@smoke' }, () => {
cy.visit('/#/markets/market-1');
});
it('Deposit to trade is visble', () => {
it('Deposit to trade is visible', () => {
cy.getByTestId('Collateral').click();
cy.contains('[data-testid="deposit"]', 'Deposit')
.should('be.visible')
.click();
cy.get('[row-id="asset-id"]').contains('tEURO').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').click();
cy.getByTestId('deposit-submit').should('be.visible');
});
});
+4 -5
View File
@@ -169,14 +169,13 @@ describe('home', { tags: '@regression' }, () => {
it('click get started button should open connect dialog', () => {
cy.getByTestId('welcome-dialog').should('be.visible');
cy.getByTestId('get-started-button').click();
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
cy.window().then((window) => {
expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
'true'
);
// @ts-ignore stub it out just for test case
window.vega = {};
cy.getByTestId('get-started-button').click();
cy.getByTestId('wallet-dialog-title').should('contain.text', 'Connect');
});
cy.getByTestId('wallet-dialog-title').should('contain.text', 'Connect');
});
});
@@ -63,6 +63,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
@@ -73,6 +74,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
describe('market order', () => {
before(() => {
cy.getByTestId(toggleMarket).click();
cy.getByTestId(placeOrderBtn).click();
});
it('must not see the price unit', function () {
@@ -48,6 +48,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-warning-auction').should(
'have.text',
'Any orders placed now will not trade until the auction ends'
@@ -60,6 +61,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-tif').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
@@ -53,7 +53,7 @@ describe('fills', { tags: '@regression' }, () => {
cy.getByTestId(tabFills).contains('Market');
cy.getByTestId(tabFills)
.get(
'[role="gridcell"][col-id="market.tradableInstrument.instrument.name"]'
'[role="gridcell"][col-id="market.tradableInstrument.instrument.code"]'
)
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
@@ -142,22 +142,22 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('sorting by Market', () => {
visitAndClickPositions();
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
const marketsSortedAsc = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'ETHBTC Quarterly (30 Jun 2022)',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
'SOLUSD',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAPL.MF21',
];
cy.getByTestId(positions).click();
// 7004-POSI-003
@@ -291,9 +291,11 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('View settlement asset', () => {
visitAndClickPositions();
cy.get('[col-id="asset"]').within(() => {
cy.get('button[type="button"]').first().click();
});
cy.get('[col-id="asset"]')
.eq(3)
.within(() => {
cy.get('button[type="button"]').click();
});
// 7004-POSI-008
cy.getByTestId(dialogContent).should('be.visible');
cy.getByTestId(dialogCloseX).click();
@@ -308,7 +310,7 @@ function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions')
.get('.ag-center-cols-container .ag-row')
.first()
.eq(multiKey ? 3 : 1)
.within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
@@ -5,118 +5,8 @@ import {
const connectVegaBtn = 'connect-vega-wallet';
const manageVegaBtn = 'manage-vega-wallet';
const form = 'rest-connector-form';
const dialogContent = 'dialog-content';
describe(
'connect hosted wallet',
{ tags: '@smoke', testIsolation: true },
() => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
cy.visit('/#/portfolio');
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
});
it('can connect', () => {
// 0002-WCON-002
// 0002-WCON-003
// 0002-WCON-039
// 0002-WCON-017
// 0002-WCON-018
// 0002-WCON-019
// Mock authentication
cy.intercept(
'POST',
'https://wallet.testnet.vega.xyz/api/v1/auth/token',
{
body: {
token: 'test-token',
},
}
);
// Mock getting keys from wallet
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
body: {
keys: [
{
algorithm: {
name: 'algo',
version: 1,
},
index: 0,
meta: [],
pub: 'HOSTED_PUBKEY',
tainted: false,
},
],
},
});
cy.getByTestId(connectVegaBtn).click();
cy.contains(
'Choose wallet app to connect, or to change port or server URL enter a custom wallet location first'
);
cy.contains('Connect Vega wallet');
cy.contains('Hosted Fairground wallet');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('user');
cy.getByTestId(form).find('#passphrase').click().type('pass');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId(manageVegaBtn).should('exist');
cy.getByTestId('manage-vega-wallet').click();
cy.getByTestId('keypair-list').should('exist');
});
it('doesnt connect with invalid credentials', () => {
// 0002-WCON-020
// Mock incorrect username/password
cy.intercept(
'POST',
'https://wallet.testnet.vega.xyz/api/v1/auth/token',
{
body: {
error: 'No wallet',
},
statusCode: 403, // 403 forbidden invalid crednetials
}
);
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('invalid name');
cy.getByTestId(form).find('#passphrase').click().type('invalid password');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
});
it('doesnt connect with empty fields', () => {
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.click();
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId(form)
.find('#wallet')
.next('[data-testid="input-error-text"]')
.should('have.text', 'Required');
cy.getByTestId(form)
.find('#passphrase')
.next('[data-testid="input-error-text"]')
.should('have.text', 'Required');
});
}
);
describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
@@ -196,8 +86,6 @@ describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('connect-vega-wallet').should('exist');
cy.getByTestId('manage-vega-wallet').should('not.exist');
cy.getByTestId('connect-vega-wallet').click();
cy.contains(
'Choose wallet app to connect, or to change port or server URL enter a custom wallet location first'
);
cy.contains('Enter a custom wallet location');
});
});
@@ -12,100 +12,108 @@ const walletTransfer = 'wallet-transfer';
const ASSET_SEPOLIA_TBTC = 2;
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
describe.skip(
'transfer fees',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
}
);
+6 -1
View File
@@ -13,9 +13,14 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+4 -1
View File
@@ -20,4 +20,7 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
+4 -1
View File
@@ -18,4 +18,7 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
+3
View File
@@ -21,3 +21,6 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+4 -1
View File
@@ -20,4 +20,7 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+4 -1
View File
@@ -19,4 +19,7 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+4 -1
View File
@@ -19,4 +19,7 @@ NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
@@ -44,16 +44,16 @@ const MainGrid = memo(
return (
<ResizableGrid vertical onChange={handleOnLayoutChange}>
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
<ResizableGrid
proportionalLayout={false}
minSize={200}
onChange={handleOnMiddleLayoutChange}
>
<ResizableGridPanel
preferredSize={sizes[0]}
priority={LayoutPriority.High}
minSize={200}
>
<ResizableGrid onChange={handleOnMiddleLayoutChange}>
<ResizableGridPanel
priority={LayoutPriority.High}
minSize={200}
preferredSize={sizesMiddle[1] || '50%'}
preferredSize={sizesMiddle[0] || '75%'}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-main-left">
@@ -74,8 +74,8 @@ const MainGrid = memo(
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
preferredSize={sizesMiddle[2] || 300}
minSize={200}
preferredSize={sizesMiddle[1] || 300}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-main-right">
@@ -91,9 +91,9 @@ const MainGrid = memo(
</ResizableGrid>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={sizes[1] || '25%'}
minSize={50}
priority={LayoutPriority.Low}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom">
@@ -411,24 +411,26 @@ describe('Closed', () => {
},
},
};
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
await act(() => {
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
);
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
);
});
await waitFor(() => {
expect(
@@ -1,27 +1,15 @@
import { OrderbookManager } from '@vegaprotocol/market-depth';
import { useCreateOrderStore } from '@vegaprotocol/orders';
import { ViewType, useSidebar } from '../sidebar';
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
const useOrderStoreRef = useCreateOrderStore();
const updateOrder = useOrderStoreRef((store) => store.update);
const updateStoredFormValues = useStopOrderFormValues(
(state) => state.update
);
const update = useDealTicketFormValues((state) => state.updateAll);
const setView = useSidebar((store) => store.setView);
return (
<OrderbookManager
marketId={marketId}
onClick={({ price, size }) => {
if (price) {
updateOrder(marketId, { price });
updateStoredFormValues(marketId, { price });
}
if (size) {
updateOrder(marketId, { size });
updateStoredFormValues(marketId, { size });
}
onClick={(values) => {
update(marketId, values);
setView({ type: ViewType.Order });
}}
/>
-3
View File
@@ -1,11 +1,9 @@
import {
RestConnector,
JsonRpcConnector,
ViewConnector,
InjectedConnector,
} from '@vegaprotocol/wallet';
export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
@@ -19,7 +17,6 @@ if (typeof window !== 'undefined') {
export const Connectors = {
injected,
rest,
jsonRpc,
view,
};
+2
View File
@@ -43,6 +43,7 @@ import { Navbar } from '../components/navbar';
import classNames from 'classnames';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner';
@@ -111,6 +112,7 @@ function AppBody({ Component }: AppProps) {
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
<ViewingBanner />
<UpgradeBanner showVersionChange={true} />
</div>
+1 -1
View File
@@ -7,7 +7,7 @@ module.exports = {
'scope-enum': async (ctx) => [
2,
'always',
['ci', 'docs', ...(await utils.getProjects(ctx))],
['ci', 'docs', 'specs', ...(await utils.getProjects(ctx))],
],
},
};
@@ -23,6 +23,7 @@ const singleRow = {
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
},
},
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
@@ -57,7 +58,7 @@ describe('BreakdownTable', () => {
});
const cells = await screen.findAllByRole('gridcell');
const expectedValues = [
'BTCUSD Monthly (30 Jun 2022)',
'BTCUSD.MF21',
'Margin',
'1,256.00 (50%)',
'1,256.00',
@@ -118,6 +119,7 @@ describe('BreakdownTable', () => {
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
},
},
},
+2 -2
View File
@@ -31,12 +31,12 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
const defs: ColDef[] = [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.name',
field: 'market.tradableInstrument.instrument.code',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
AccountFields,
'market.tradableInstrument.instrument.name'
'market.tradableInstrument.instrument.code'
>) => {
if (!value) return 'None';
return value;
+6 -3
View File
@@ -121,9 +121,12 @@ export function createClient({
// called on startup, NodeCheck and NodeCheckTimeUpdate are called
// by the NodeSwitcher component and the useNodeHealth hook
if (
['NodeGuard', 'NodeCheck', 'NodeCheckTimeUpdate'].includes(
operation.operationName
)
[
'NodeGuard',
'NodeCheck',
'NodeCheckTimeUpdate',
'BlockStatistics',
].includes(operation.operationName)
) {
return;
}
+2 -2
View File
@@ -28,10 +28,10 @@ export const assetProvider = makeDataProvider<
getData,
});
export const useAssetDataProvider = (assetId: string) => {
export const useAssetDataProvider = (assetId: string, skip?: boolean) => {
return useDataProvider({
dataProvider: assetProvider,
variables: { assetId: assetId || '' },
skip: !assetId,
skip: !assetId || skip,
});
};
@@ -10,14 +10,11 @@ declare global {
export const addConnectPublicKey = () => {
Cypress.Commands.add('connectPublicKey', (publicKey) => {
const connectVegaWaletBtn = Cypress.$(
`[data-testid="connect-vega-wallet"]`
);
const connectVegaWaletBtn = Cypress.$(`[data-testid="view-as-user"]`);
if (connectVegaWaletBtn.length > 0) {
cy.get('aside [data-testid="connect-vega-wallet"]').click();
cy.getByTestId('connector-view').should('be.visible').click();
cy.getByTestId('address').click();
cy.getByTestId('address').type(publicKey);
cy.get('aside button').contains('View as party').click();
cy.getByTestId('address').should('be.visible').focus();
cy.getByTestId('address').type(publicKey, { delay: 50 });
cy.getByTestId('connect').click();
}
});
@@ -29,7 +29,7 @@ export const mockConnectWallet = () => {
export const mockConnectWalletWithUserError = () => {
cy.mockWallet((req) => {
aliasWalletConnectWithUserError(req);
aliasWalletConnectWithUserError(req, Cypress.env('VEGA_WALLET_API_TOKEN'));
});
};
+34 -1
View File
@@ -62,10 +62,27 @@ export const aliasWalletConnectQuery = (
},
});
}
if (hasMethod(req, 'client.get_chain_id')) {
req.reply({
statusCode: 200,
headers: {
'Access-Control-Expose-Headers': 'Authorization',
Authorization: `VWT ${token}`,
},
body: {
jsonrpc: '2.0',
result: {
chainID: 'test-id',
},
id: '1',
},
});
}
};
export const aliasWalletConnectWithUserError = (
req: CyHttpMessages.IncomingHttpRequest
req: CyHttpMessages.IncomingHttpRequest,
token: string
) => {
if (hasMethod(req, 'client.connect_wallet')) {
req.alias = 'client.connect_wallet';
@@ -82,4 +99,20 @@ export const aliasWalletConnectWithUserError = (
},
});
}
if (hasMethod(req, 'client.get_chain_id')) {
req.reply({
statusCode: 200,
headers: {
'Access-Control-Expose-Headers': 'Authorization',
Authorization: `VWT ${token}`,
},
body: {
jsonrpc: '2.0',
result: {
chainID: 'test-id',
},
id: '1',
},
});
}
};
@@ -3,29 +3,25 @@ import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
import * as Schema from '@vegaprotocol/types';
import type { OrderObj } from '@vegaprotocol/orders';
import type { OrderFormFields } from '../../hooks/use-order-form';
import type { OrderFormValues } from '../../hooks/use-form-values';
export interface DealTicketAmountProps {
control: Control<OrderFormFields>;
orderType: Schema.OrderType;
control: Control<OrderFormValues>;
type: Schema.OrderType;
marketData: StaticMarketData;
marketPrice?: string;
market: Market;
sizeError?: string;
priceError?: string;
update: (obj: Partial<OrderObj>) => void;
size: string;
price?: string;
}
export const DealTicketAmount = ({
orderType,
type,
marketData,
marketPrice,
...props
}: DealTicketAmountProps) => {
switch (orderType) {
switch (type) {
case Schema.OrderType.TYPE_MARKET:
return (
<DealTicketMarketAmount
@@ -37,7 +33,7 @@ export const DealTicketAmount = ({
case Schema.OrderType.TYPE_LIMIT:
return <DealTicketLimitAmount {...props} />;
default: {
throw new Error('Invalid ticket type');
throw new Error('Invalid ticket type ' + type);
}
}
};
@@ -1,8 +1,8 @@
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import {
DealTicketType,
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
isStopOrderType,
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import { StopOrder } from './deal-ticket-stop-order';
import {
useStaticMarketData,
@@ -25,7 +25,9 @@ export const DealTicketContainer = ({
marketId,
...props
}: DealTicketContainerProps) => {
const type = useDealTicketTypeStore((state) => state.type[marketId]);
const showStopOrder = useDealTicketFormValues((state) =>
isStopOrderType(state.formValues[marketId]?.type)
);
const {
data: market,
error: marketError,
@@ -48,9 +50,7 @@ export const DealTicketContainer = ({
reload={reload}
>
{market && marketData ? (
FLAGS.STOP_ORDERS &&
(type === DealTicketType.StopLimit ||
type === DealTicketType.StopMarket) ? (
FLAGS.STOP_ORDERS && showStopOrder ? (
<StopOrder
market={market}
marketPrice={marketPrice}
@@ -5,8 +5,8 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
export type DealTicketLimitAmountProps = Omit<
Omit<DealTicketAmountProps, 'marketData'>,
'orderType'
DealTicketAmountProps,
'marketData' | 'type'
>;
export const DealTicketLimitAmount = ({
@@ -14,9 +14,6 @@ export const DealTicketLimitAmount = ({
market,
sizeError,
priceError,
update,
price,
size,
}: DealTicketLimitAmountProps) => {
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
@@ -62,17 +59,16 @@ export const DealTicketLimitAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={() => (
render={({ field }) => (
<Input
id="input-order-size-limit"
className="w-full"
type="number"
value={size}
onChange={(e) => update({ size: e.target.value })}
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -95,19 +91,17 @@ export const DealTicketLimitAmount = ({
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
// @ts-ignore this fulfills the interface but still errors
validate: validateAmount(priceStep, 'Price'),
}}
render={() => (
render={({ field }) => (
<Input
id="input-price-quote"
className="w-full"
type="number"
value={price}
onChange={(e) => update({ price: e.target.value })}
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -10,10 +10,7 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
import classNames from 'classnames';
export type DealTicketMarketAmountProps = Omit<
DealTicketAmountProps,
'orderType'
>;
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
export const DealTicketMarketAmount = ({
control,
@@ -21,8 +18,6 @@ export const DealTicketMarketAmount = ({
marketData,
marketPrice,
sizeError,
update,
size,
}: DealTicketMarketAmountProps) => {
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const sizeStep = toDecimal(market?.positionDecimalPlaces);
@@ -50,17 +45,16 @@ export const DealTicketMarketAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={() => (
render={({ field }) => (
<Input
id="input-order-size-market"
className="w-full"
type="number"
value={size}
onChange={(e) => update({ size: e.target.value })}
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
{...field}
/>
)}
/>
@@ -1,7 +1,6 @@
import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets';
import type { OrderObj } from '@vegaprotocol/orders';
import type { OrderFormFields } from '../../hooks/use-order-form';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -12,25 +11,21 @@ import {
} from '@vegaprotocol/ui-toolkit';
export interface DealTicketSizeIcebergProps {
control: Control<OrderFormFields>;
control: Control<OrderFormValues>;
market: Market;
peakSizeError?: string;
minimumVisibleSizeError?: string;
update: (obj: Partial<OrderObj>) => void;
peakSize: string;
minimumVisibleSize: string;
size: string;
peakSize?: string;
}
export const DealTicketSizeIceberg = ({
control,
market,
update,
peakSizeError,
minimumVisibleSizeError,
peakSize,
minimumVisibleSize,
size,
peakSize,
}: DealTicketSizeIcebergProps) => {
const sizeStep = toDecimal(market?.positionDecimalPlaces);
@@ -80,7 +75,7 @@ export const DealTicketSizeIceberg = ({
className="!mb-1"
>
<Controller
name="icebergOpts.peakSize"
name="peakSize"
control={control}
rules={{
required: t('You need to provide a peak size'),
@@ -97,25 +92,17 @@ export const DealTicketSizeIceberg = ({
},
validate: validateAmount(sizeStep, 'peakSize'),
}}
render={() => (
render={({ field }) => (
<Input
id="input-order-peak-size"
className="w-full"
type="number"
value={peakSize}
onChange={(e) =>
update({
icebergOpts: {
peakSize: e.target.value,
minimumVisibleSize,
},
})
}
step={sizeStep}
min={sizeStep}
max={size}
data-testid="order-peak-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -144,7 +131,7 @@ export const DealTicketSizeIceberg = ({
className="!mb-1"
>
<Controller
name="icebergOpts.minimumVisibleSize"
name="minimumVisibleSize"
control={control}
rules={{
required: t('You need to provide a minimum visible size'),
@@ -154,7 +141,7 @@ export const DealTicketSizeIceberg = ({
'Minimum visible size cannot be lower than ' + sizeStep
),
},
max: {
max: peakSize && {
value: peakSize,
message: t(
'Minimum visible size cannot be greater than the peak size (%s)',
@@ -163,25 +150,17 @@ export const DealTicketSizeIceberg = ({
},
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
}}
render={() => (
render={({ field }) => (
<Input
id="input-order-minimum-size"
className="w-full"
type="number"
value={minimumVisibleSize}
onChange={(e) =>
update({
icebergOpts: {
peakSize,
minimumVisibleSize: e.target.value,
},
})
}
step={sizeStep}
min={sizeStep}
max={peakSize}
data-testid="order-minimum-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -6,8 +6,11 @@ import { generateMarket } from '../../test-helpers';
import { StopOrder } from './deal-ticket-stop-order';
import * as Schema from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing';
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
import type { StopOrderFormValues } from '../../hooks/use-form-values';
import {
DealTicketType,
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import type { FeatureFlags } from '@vegaprotocol/environment';
jest.mock('zustand');
@@ -131,9 +134,11 @@ describe('StopOrder', () => {
expiresAt: '2023-07-27T16:43:27.000',
};
useStopOrderFormValues.setState({
useDealTicketFormValues.setState({
formValues: {
[market.id]: values,
[market.id]: {
[DealTicketType.StopLimit]: values,
},
},
});
@@ -207,11 +212,13 @@ describe('StopOrder', () => {
// switch to market order type error should disappear
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// switch back to limit type
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeLimit));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
@@ -1,4 +1,3 @@
import type { FormEventHandler } from 'react';
import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
@@ -8,7 +7,7 @@ import {
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import { useForm, Controller } from 'react-hook-form';
import { useForm, Controller, useController } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
Radio,
@@ -24,22 +23,22 @@ import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
import { timeInForceLabel } from '@vegaprotocol/orders';
import {
NoWalletWarning,
REDUCE_ONLY_TOOLTIP,
useNotionalSize,
stopSubmit,
getNotionalSize,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
useStopOrderFormValues,
type StopOrderFormValues,
} from '../../hooks/use-stop-order-form-values';
import {
useDealTicketFormValues,
DealTicketType,
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
type StopOrderFormValues,
dealTicketTypeToOrderType,
isStopOrderType,
} from '../../hooks/use-form-values';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import { validateExpiration } from '../../utils';
@@ -50,32 +49,36 @@ export interface StopOrderProps {
submit: (order: StopOrdersSubmission) => void;
}
const defaultValues: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_LIMIT,
const getDefaultValues = (
type: Schema.OrderType,
storedValues?: Partial<StopOrderFormValues>
): StopOrderFormValues => ({
type,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
triggerType: 'price',
triggerDirection:
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
expire: false,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
size: '0',
};
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
...storedValues,
});
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
const [, updateOrder] = useOrder(market.id);
const updateStoredFormValues = useStopOrderFormValues(
(state) => state.update
const setType = useDealTicketFormValues((state) => state.setType);
const updateStoredFormValues = useDealTicketFormValues(
(state) => state.updateStopOrder
);
const storedFormValues = useStopOrderFormValues(
const storedFormValues = useDealTicketFormValues(
(state) => state.formValues[market.id]
);
const { handleSubmit, setValue, watch, control, formState } =
const dealTicketType = storedFormValues?.type ?? DealTicketType.StopLimit;
const type = dealTicketTypeToOrderType(dealTicketType);
const { handleSubmit, setValue, watch, control, formState, reset } =
useForm<StopOrderFormValues>({
defaultValues: { ...defaultValues, ...storedFormValues },
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
});
const { errors } = formState;
const lastSubmitTime = useRef(0);
@@ -102,16 +105,22 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const triggerType = watch('triggerType');
const triggerPrice = watch('triggerPrice');
const timeInForce = watch('timeInForce');
const type = watch('type');
const rawPrice = watch('price');
const rawSize = watch('size');
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
setValue('size', storedFormValues.size);
}
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
setValue('price', storedFormValues.price);
}
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
if (size && rawSize !== size) {
setValue('size', size);
}
}, [storedFormValues, dealTicketType, rawSize, setValue]);
useEffect(() => {
const price = storedFormValues?.[dealTicketType]?.price;
if (price && rawPrice !== price) {
setValue('price', price);
}
}, [storedFormValues, dealTicketType, rawPrice, setValue]);
const isPriceTrigger = triggerType === 'price';
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
@@ -127,7 +136,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
: marketPrice
);
const notionalSize = useNotionalSize(
const notionalSize = getNotionalSize(
price,
size,
market.decimalPlaces,
@@ -153,47 +162,28 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
? formatNumber(triggerPrice, market.decimalPlaces)
: undefined;
useController({
name: 'type',
control,
});
return (
<form
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
noValidate
>
<Controller
name="type"
control={control}
render={({ field }) => {
const { value } = field;
return (
<TypeToggle
value={
value === Schema.OrderType.TYPE_LIMIT
? DealTicketType.StopLimit
: DealTicketType.StopMarket
}
onValueChange={(value) => {
const type = value as DealTicketType;
setDealTicketType(market.id, type);
if (
type === DealTicketType.Limit ||
type === DealTicketType.Market
) {
updateOrder({
type:
type === DealTicketType.Limit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET,
});
return;
}
setValue(
'type',
type === DealTicketType.StopLimit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET
);
}}
/>
);
<TypeToggle
value={dealTicketType}
onValueChange={(dealTicketType) => {
setType(market.id, dealTicketType);
if (isStopOrderType(dealTicketType)) {
reset(
getDefaultValues(
dealTicketTypeToOrderType(dealTicketType),
storedFormValues?.[dealTicketType]
)
);
}
}}
/>
{errors.type && (
@@ -1,12 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
act,
render,
renderHook,
screen,
waitFor,
} from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket, generateMarketData } from '../../test-helpers';
import { DealTicket } from './deal-ticket';
@@ -15,7 +9,10 @@ import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { addDecimal } from '@vegaprotocol/utils';
import type { OrdersQuery } from '@vegaprotocol/orders';
import { useCreateOrderStore } from '@vegaprotocol/orders';
import {
DealTicketType,
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import * as positionsTools from '@vegaprotocol/positions';
import { OrdersDocument } from '@vegaprotocol/orders';
@@ -50,9 +47,6 @@ function generateJsx(mocks: MockedResponse[] = []) {
}
describe('DealTicket', () => {
const { result } = renderHook(() => useCreateOrderStore());
const useOrderStore = result.current;
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
@@ -166,9 +160,11 @@ describe('DealTicket', () => {
persist: true,
};
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
},
});
@@ -204,9 +200,11 @@ describe('DealTicket', () => {
reduceOnly: true,
postOnly: false,
};
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
},
});
@@ -247,9 +245,11 @@ describe('DealTicket', () => {
postOnly: true,
};
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
},
});
@@ -295,9 +295,11 @@ describe('DealTicket', () => {
},
};
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
},
});
@@ -339,9 +341,11 @@ describe('DealTicket', () => {
reduceOnly: false,
postOnly: false,
};
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
},
});
@@ -370,6 +374,7 @@ describe('DealTicket', () => {
expect(screen.getByTestId('iceberg')).not.toBeChecked();
});
// eslint-disable-next-line jest/no-disabled-tests
it('handles TIF select box dependent on order type', async () => {
render(generateJsx());
@@ -1,7 +1,8 @@
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import type { FormEventHandler } from 'react';
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
import { Controller, useController, useForm } from 'react-hook-form';
import { DealTicketAmount } from './deal-ticket-amount';
import { DealTicketButton } from './deal-ticket-button';
import {
@@ -13,7 +14,8 @@ import { SideSelector } from './side-selector';
import { TimeInForceSelector } from './time-in-force-selector';
import { TypeSelector } from './type-selector';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { normalizeOrderSubmission, useVegaWallet } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
import {
Checkbox,
InputError,
@@ -51,14 +53,15 @@ import {
useAccountBalance,
} from '@vegaprotocol/accounts';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import { useOrderForm } from '../../hooks/use-order-form';
import { OrderType } from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
DealTicketType,
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
dealTicketTypeToOrderType,
isStopOrderType,
} from '../../hooks/use-form-values';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { useDealTicketFormValues } from '../../hooks/use-form-values';
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop';
@@ -75,23 +78,42 @@ export interface DealTicketProps {
onDeposit: (assetId: string) => void;
}
export const useNotionalSize = (
export const getNotionalSize = (
price: string | null | undefined,
size: string | undefined,
decimalPlaces: number,
positionDecimalPlaces: number
) =>
useMemo(() => {
if (price && size) {
return removeDecimal(
toBigNum(size, positionDecimalPlaces).multipliedBy(
toBigNum(price, decimalPlaces)
),
decimalPlaces
);
}
return null;
}, [price, size, decimalPlaces, positionDecimalPlaces]);
) => {
if (price && size) {
return removeDecimal(
toBigNum(size, positionDecimalPlaces).multipliedBy(
toBigNum(price, decimalPlaces)
),
decimalPlaces
);
}
return null;
};
export const stopSubmit: FormEventHandler = (e) => e.preventDefault();
const getDefaultValues = (
type: Schema.OrderType,
storedValues?: Partial<OrderFormValues>
): OrderFormValues => ({
type,
side: Schema.Side.SIDE_BUY,
timeInForce:
type === Schema.OrderType.TYPE_LIMIT
? Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
size: '0',
price: '0',
expiresAt: undefined,
postOnly: false,
reduceOnly: false,
...storedValues,
});
export const DealTicket = ({
market,
@@ -103,32 +125,29 @@ export const DealTicket = ({
onDeposit,
}: DealTicketProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
const updateStopOrderFormValues = useStopOrderFormValues(
(state) => state.update
const setType = useDealTicketFormValues((state) => state.setType);
const storedFormValues = useDealTicketFormValues(
(state) => state.formValues[market.id]
);
// store last used tif for market so that when changing OrderType the previous TIF
// selection for that type is used when switching back
const [lastTIF, setLastTIF] = useState({
[OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC,
[OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC,
});
const updateStoredFormValues = useDealTicketFormValues(
(state) => state.updateOrder
);
const dealTicketType = storedFormValues?.type ?? DealTicketType.Limit;
const type = dealTicketTypeToOrderType(dealTicketType);
const {
control,
errors,
order,
setError,
clearErrors,
update,
reset,
formState: { errors },
handleSubmit,
} = useOrderForm(market.id);
setValue,
watch,
} = useForm<OrderFormValues>({
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
});
const lastSubmitTime = useRef(0);
const asset = market.tradableInstrument.instrument.product.settlementAsset;
const {
accountBalance: marginAccountBalance,
loading: loadingMarginAccountBalance,
@@ -144,24 +163,54 @@ export const DealTicket = ({
).toString();
const { marketState, marketTradingMode } = marketData;
const timeInForce = watch('timeInForce');
const normalizedOrder =
order &&
normalizeOrderSubmission(
order,
market.decimalPlaces,
market.positionDecimalPlaces
);
const side = watch('side');
const rawSize = watch('size');
const rawPrice = watch('price');
const iceberg = watch('iceberg');
const peakSize = watch('peakSize');
const price = useMemo(() => {
return (
normalizedOrder &&
marketPrice &&
getDerivedPrice(normalizedOrder, marketPrice)
);
}, [normalizedOrder, marketPrice]);
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
if (size && rawSize !== size) {
setValue('size', size);
}
}, [storedFormValues, dealTicketType, rawSize, setValue]);
const notionalSize = useNotionalSize(
useEffect(() => {
const price = storedFormValues?.[dealTicketType]?.price;
if (price && rawPrice !== price) {
setValue('price', price);
}
}, [storedFormValues, dealTicketType, rawPrice, setValue]);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
updateStoredFormValues(market.id, value);
});
return () => subscription.unsubscribe();
}, [watch, market.id, updateStoredFormValues]);
const normalizedOrder = mapFormValuesToOrderSubmission(
{
price: rawPrice || undefined,
side,
size: rawSize,
timeInForce,
type,
},
market.id,
market.decimalPlaces,
market.positionDecimalPlaces
);
const price =
normalizedOrder &&
marketPrice &&
getDerivedPrice(normalizedOrder, marketPrice);
const notionalSize = getNotionalSize(
price,
normalizedOrder?.size,
market.decimalPlaces,
@@ -205,22 +254,20 @@ export const DealTicket = ({
const assetSymbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
useEffect(() => {
const summaryError = useMemo(() => {
if (!pubKey) {
setError('summary', {
return {
message: t('No public key selected'),
type: SummaryValidationType.NoPubKey,
});
return;
};
}
const marketStateError = validateMarketState(marketState);
if (marketStateError !== true) {
setError('summary', {
return {
message: marketStateError,
type: SummaryValidationType.MarketState,
});
return;
};
}
const hasNoBalance =
@@ -229,24 +276,21 @@ export const DealTicket = ({
hasNoBalance &&
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
) {
setError('summary', {
return {
message: SummaryValidationType.NoCollateral,
type: SummaryValidationType.NoCollateral,
});
return;
};
}
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
if (marketTradingModeError !== true) {
setError('summary', {
return {
message: marketTradingModeError,
type: SummaryValidationType.TradingMode,
});
return;
};
}
// No error found above clear the error in case it was active on a previous render
clearErrors('summary');
return undefined;
}, [
marketState,
marketTradingMode,
@@ -255,156 +299,83 @@ export const DealTicket = ({
loadingMarginAccountBalance,
loadingGeneralAccountBalance,
pubKey,
setError,
clearErrors,
]);
const disablePostOnlyCheckbox = useMemo(() => {
const disabled = order
? [
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(order.timeInForce)
: true;
return disabled;
}, [order]);
const disablePostOnlyCheckbox = [
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(timeInForce);
const disableReduceOnlyCheckbox = useMemo(() => {
const disabled = order
? ![
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(order.timeInForce)
: true;
return disabled;
}, [order]);
const disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
const onSubmit = useCallback(
(order: OrderSubmission) => {
(formValues: OrderFormValues) => {
const now = new Date().getTime();
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
return;
}
submit(
normalizeOrderSubmission(
order,
mapFormValuesToOrderSubmission(
formValues,
market.id,
market.decimalPlaces,
market.positionDecimalPlaces
)
);
lastSubmitTime.current = now;
},
[submit, market.decimalPlaces, market.positionDecimalPlaces]
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
);
// if an order doesn't exist one will be created by the store immediately
if (!order || !normalizedOrder) {
return null;
}
useController({
name: 'type',
control,
rules: {
validate: validateType(marketData.marketTradingMode, marketData.trigger),
},
});
return (
<form
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
onSubmit={
isReadOnly || !pubKey
? stopSubmit
: handleSubmit(summaryError ? noop : onSubmit)
}
noValidate
data-testid="deal-ticket-form"
>
<Controller
name="type"
control={control}
rules={{
validate: validateType(
marketData.marketTradingMode,
marketData.trigger
),
<TypeSelector
value={dealTicketType}
onValueChange={(dealTicketType) => {
setType(market.id, dealTicketType);
if (!isStopOrderType(dealTicketType)) {
reset(
getDefaultValues(
dealTicketTypeToOrderType(dealTicketType),
storedFormValues?.[dealTicketType]
)
);
}
}}
render={() => (
<TypeSelector
value={
order.type === OrderType.TYPE_LIMIT
? DealTicketType.Limit
: DealTicketType.Market
}
onValueChange={(dealTicketType) => {
setDealTicketType(market.id, dealTicketType);
if (
dealTicketType !== DealTicketType.Limit &&
dealTicketType !== DealTicketType.Market
) {
updateStopOrderFormValues(market.id, {
type:
dealTicketType === DealTicketType.StopLimit
? OrderType.TYPE_LIMIT
: OrderType.TYPE_MARKET,
});
return;
}
const type =
dealTicketType === DealTicketType.Limit
? OrderType.TYPE_LIMIT
: OrderType.TYPE_MARKET;
update({
type,
// when changing type also update the TIF to what was last used of new type
timeInForce: lastTIF[type] || order.timeInForce,
postOnly:
type === OrderType.TYPE_MARKET ? false : order.postOnly,
iceberg:
type === OrderType.TYPE_MARKET ||
[
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(lastTIF[type] || order.timeInForce)
? false
: order.iceberg,
icebergOpts:
type === OrderType.TYPE_MARKET ||
[
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(lastTIF[type] || order.timeInForce)
? undefined
: order.icebergOpts,
reduceOnly:
type === OrderType.TYPE_LIMIT &&
![
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(lastTIF[type] || order.timeInForce)
? false
: order.postOnly,
expiresAt: undefined,
});
clearErrors(['expiresAt', 'price']);
}}
market={market}
marketData={marketData}
errorMessage={errors.type?.message}
/>
)}
market={market}
marketData={marketData}
errorMessage={errors.type?.message}
/>
<Controller
name="side"
control={control}
render={() => (
<SideSelector
value={order.side}
onValueChange={(side) => {
update({ side });
}}
/>
render={({ field }) => (
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<DealTicketAmount
type={type}
control={control}
orderType={order.type}
market={market}
marketData={marketData}
marketPrice={marketPrice || undefined}
sizeError={errors.size?.message}
priceError={errors.price?.message}
update={update}
size={order.size}
price={order.price}
/>
<Controller
name="timeInForce"
@@ -415,58 +386,29 @@ export const DealTicket = ({
marketData.trigger
),
}}
render={() => (
render={({ field }) => (
<TimeInForceSelector
value={order.timeInForce}
orderType={order.type}
onSelect={(timeInForce) => {
// Reset post only and reduce only when changing TIF
update({
timeInForce,
postOnly: [
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(timeInForce)
? false
: order.postOnly,
reduceOnly: ![
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(timeInForce)
? false
: order.reduceOnly,
});
// Set TIF value for the given order type, so that when switching
// types we know the last used TIF for the given order type
setLastTIF((curr) => ({
...curr,
[order.type]: timeInForce,
expiresAt: undefined,
}));
clearErrors('expiresAt');
}}
value={field.value}
orderType={type}
onSelect={field.onChange}
market={market}
marketData={marketData}
errorMessage={errors.timeInForce?.message}
/>
)}
/>
{order.type === Schema.OrderType.TYPE_LIMIT &&
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
{type === Schema.OrderType.TYPE_LIMIT &&
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller
name="expiresAt"
control={control}
rules={{
validate: validateExpiration,
}}
render={() => (
render={({ field }) => (
<ExpirySelector
value={order.expiresAt}
onSelect={(expiresAt) =>
update({
expiresAt: expiresAt || undefined,
})
}
value={field.value}
onSelect={(expiresAt) => field.onChange(expiresAt)}
errorMessage={errors.expiresAt?.message}
/>
)}
@@ -476,13 +418,14 @@ export const DealTicket = ({
<Controller
name="postOnly"
control={control}
render={() => (
render={({ field }) => (
<Checkbox
name="post-only"
checked={order.postOnly}
checked={!disablePostOnlyCheckbox && field.value}
disabled={disablePostOnlyCheckbox}
onCheckedChange={() => {
update({ postOnly: !order.postOnly, reduceOnly: false });
onCheckedChange={(postOnly) => {
field.onChange(postOnly);
setValue('reduceOnly', false);
}}
label={
<Tooltip
@@ -507,13 +450,14 @@ export const DealTicket = ({
<Controller
name="reduceOnly"
control={control}
render={() => (
render={({ field }) => (
<Checkbox
name="reduce-only"
checked={order.reduceOnly}
checked={!disableReduceOnlyCheckbox && field.value}
disabled={disableReduceOnlyCheckbox}
onCheckedChange={() => {
update({ postOnly: false, reduceOnly: !order.reduceOnly });
onCheckedChange={(reduceOnly) => {
field.onChange(reduceOnly);
setValue('postOnly', false);
}}
label={
<Tooltip
@@ -534,53 +478,49 @@ export const DealTicket = ({
)}
/>
</div>
<div className="flex gap-2 pb-2 justify-between">
{order.type === Schema.OrderType.TYPE_LIMIT && (
<Controller
name="iceberg"
control={control}
render={() => (
<Checkbox
name="iceberg"
checked={order.iceberg}
onCheckedChange={() => {
update({ iceberg: !order.iceberg, icebergOpts: undefined });
}}
label={
<Tooltip
description={
<p>
{t(`Trade only a fraction of the order size at once.
{type === Schema.OrderType.TYPE_LIMIT && (
<>
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="iceberg"
control={control}
render={({ field }) => (
<Checkbox
name="iceberg"
checked={field.value}
onCheckedChange={field.onChange}
label={
<Tooltip
description={
<p>
{t(`Trade only a fraction of the order size at once.
After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away.
For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each.
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}
</p>
}
>
<span className="text-xs">{t('Iceberg')}</span>
</Tooltip>
}
/>
)}
/>
)}
</div>
{order.iceberg && (
<DealTicketSizeIceberg
update={update}
market={market}
peakSizeError={errors.icebergOpts?.peakSize?.message}
minimumVisibleSizeError={
errors.icebergOpts?.minimumVisibleSize?.message
}
control={control}
size={order.size}
peakSize={order.icebergOpts?.peakSize || ''}
minimumVisibleSize={order.icebergOpts?.minimumVisibleSize || ''}
/>
</p>
}
>
<span className="text-xs">{t('Iceberg')}</span>
</Tooltip>
}
/>
)}
/>
</div>
{iceberg && (
<DealTicketSizeIceberg
market={market}
peakSizeError={errors.peakSize?.message}
minimumVisibleSizeError={errors.minimumVisibleSize?.message}
control={control}
size={rawSize}
peakSize={peakSize}
/>
)}
</>
)}
<SummaryMessage
errorMessage={errors.summary?.message}
error={summaryError}
asset={asset}
marketTradingMode={marketData.marketTradingMode}
balance={balance}
@@ -593,7 +533,7 @@ export const DealTicket = ({
onClickCollateral={onClickCollateral}
onDeposit={onDeposit}
/>
<DealTicketButton side={order.side} />
<DealTicketButton side={side} />
<DealTicketFeeDetails
order={
normalizedOrder && { ...normalizedOrder, price: price || undefined }
@@ -619,7 +559,7 @@ export const DealTicket = ({
* renders warnings about current state of the market
*/
interface SummaryMessageProps {
errorMessage?: string;
error?: { message: string; type: string };
asset: { id: string; symbol: string; name: string; decimals: number };
marketTradingMode: MarketData['marketTradingMode'];
balance: string;
@@ -649,7 +589,7 @@ export const NoWalletWarning = ({
const SummaryMessage = memo(
({
errorMessage,
error,
asset,
marketTradingMode,
balance,
@@ -665,7 +605,7 @@ const SummaryMessage = memo(
return <NoWalletWarning isReadOnly={isReadOnly} />;
}
if (errorMessage === SummaryValidationType.NoCollateral) {
if (error?.type === SummaryValidationType.NoCollateral) {
return (
<div className="mb-2">
<ZeroBalanceError
@@ -679,11 +619,11 @@ const SummaryMessage = memo(
// If we have any other full error which prevents
// submission render that first
if (errorMessage) {
if (error?.message) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
{errorMessage}
{error?.message}
</InputError>
</div>
);
@@ -21,6 +21,13 @@ interface TimeInForceSelectorProps {
errorMessage?: string;
}
const typeLimitOptions = Object.entries(Schema.OrderTimeInForce);
const typeMarketOptions = typeLimitOptions.filter(
([_, timeInForce]) =>
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
);
export const TimeInForceSelector = ({
value,
orderType,
@@ -31,12 +38,8 @@ export const TimeInForceSelector = ({
}: TimeInForceSelectorProps) => {
const options =
orderType === Schema.OrderType.TYPE_LIMIT
? Object.entries(Schema.OrderTimeInForce)
: Object.entries(Schema.OrderTimeInForce).filter(
([_, timeInForce]) =>
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
);
? typeLimitOptions
: typeMarketOptions;
const renderError = (errorType: string) => {
if (errorType === MarketModeValidationType.Auction) {
@@ -16,7 +16,7 @@ import { t } from '@vegaprotocol/i18n';
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { compileGridData } from '../trading-mode-tooltip';
import { MarketModeValidationType } from '../../constants';
import { DealTicketType } from '../../hooks/use-type-store';
import { DealTicketType } from '../../hooks/use-form-values';
import * as RadioGroup from '@radix-ui/react-radio-group';
import classNames from 'classnames';
import { FLAGS } from '@vegaprotocol/environment';
+1 -2
View File
@@ -1,4 +1,3 @@
export * from './__generated__/EstimateOrder';
export * from './use-estimate-fees';
export * from './use-type-store';
export * from './use-stop-order-form-values';
export * from './use-form-values';
@@ -0,0 +1,144 @@
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import type { OrderTimeInForce, Side, OrderType } from '@vegaprotocol/types';
import * as Schema from '@vegaprotocol/types';
import { immer } from 'zustand/middleware/immer';
export enum DealTicketType {
Limit = 'Limit',
Market = 'Market',
StopLimit = 'StopLimit',
StopMarket = 'StopMarket',
}
export interface StopOrderFormValues {
side: Side;
triggerDirection: Schema.StopOrderTriggerDirection;
triggerType: 'price' | 'trailingPercentOffset';
triggerPrice?: string;
triggerTrailingPercentOffset?: string;
type: OrderType;
size: string;
timeInForce: OrderTimeInForce;
price?: string;
expire: boolean;
expiryStrategy?: Schema.StopOrderExpiryStrategy;
expiresAt?: string;
}
export type OrderFormValues = {
type: OrderType;
side: Side;
size: string;
timeInForce: OrderTimeInForce;
price?: string;
expiresAt?: string | undefined;
postOnly?: boolean;
reduceOnly?: boolean;
iceberg?: boolean;
peakSize?: string;
minimumVisibleSize?: string;
};
type UpdateOrder = (marketId: string, values: Partial<OrderFormValues>) => void;
type UpdateStopOrder = (
marketId: string,
values: Partial<StopOrderFormValues>
) => void;
type Store = {
updateOrder: UpdateOrder;
updateStopOrder: UpdateStopOrder;
setType: (marketId: string, value: DealTicketType) => void;
updateAll: (
marketId: string,
values: { size?: string; price?: string }
) => void;
formValues: Record<
string,
| {
[DealTicketType.Limit]?: Partial<OrderFormValues>;
[DealTicketType.Market]?: Partial<OrderFormValues>;
[DealTicketType.StopLimit]?: Partial<StopOrderFormValues>;
[DealTicketType.StopMarket]?: Partial<StopOrderFormValues>;
type?: DealTicketType;
}
| undefined
>;
};
export const dealTicketTypeToOrderType = (dealTicketType?: DealTicketType) =>
dealTicketType === DealTicketType.Limit ||
dealTicketType === DealTicketType.StopLimit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET;
export const isStopOrderType = (dealTicketType?: DealTicketType) =>
dealTicketType === DealTicketType.StopLimit ||
dealTicketType === DealTicketType.StopMarket;
export const useDealTicketFormValues = create<Store>()(
immer(
persist(
subscribeWithSelector((set) => ({
formValues: {},
updateStopOrder: (marketId, formValues) => {
set((state) => {
const type =
formValues.type === Schema.OrderType.TYPE_LIMIT
? DealTicketType.StopLimit
: DealTicketType.StopMarket;
const market = state.formValues[marketId] || {};
if (!state.formValues[marketId]) {
state.formValues[marketId] = market;
}
market[type] = Object.assign(market[type] ?? {}, formValues);
});
},
updateOrder: (marketId, formValues) => {
set((state) => {
const type =
formValues.type === Schema.OrderType.TYPE_LIMIT
? DealTicketType.Limit
: DealTicketType.Market;
const market = state.formValues[marketId] || {};
if (!state.formValues[marketId]) {
state.formValues[marketId] = market;
}
market[type] = Object.assign(market[type] ?? {}, formValues);
});
},
updateAll: (
marketId: string,
formValues: { size?: string; price?: string }
) => {
set((state) => {
const market = state.formValues[marketId] || {};
if (!state.formValues[marketId]) {
state.formValues[marketId] = market;
}
for (const type of Object.values(DealTicketType)) {
market[type] = Object.assign(market[type] ?? {}, formValues);
}
});
},
setType: (marketId, type) => {
set((state) => {
state.formValues[marketId] = Object.assign(
state.formValues[marketId] ?? {},
{ type }
);
});
},
})),
{
name: 'vega_deal_ticket_store',
}
)
)
);
@@ -1,68 +0,0 @@
import omit from 'lodash/omit';
import { act, renderHook } from '@testing-library/react';
import { getDefaultOrder, useCreateOrderStore } from '@vegaprotocol/orders';
import { useOrderForm } from './use-order-form';
jest.mock('zustand');
describe('useOrderForm', () => {
const marketId = 'market-id';
const setup = (marketId: string) => {
return renderHook(() => useOrderForm(marketId));
};
const { result } = renderHook(() => useCreateOrderStore());
const useOrderStore = result.current;
it('updates form fields when the order changes', async () => {
const order = getDefaultOrder(marketId);
const { result } = setup(marketId);
// expect default values
expect(result.current.order).toEqual(order);
expect(result.current.getValues()).toEqual(order);
const priceUpdate = {
...order,
price: '100',
size: '22',
};
await act(async () => {
useOrderStore.setState({
orders: {
[marketId]: priceUpdate,
},
});
});
// check order store has updated fields
expect(result.current.order).toEqual(priceUpdate);
// check react-hook-form has updated fields
expect(result.current.getValues()).toEqual(priceUpdate);
});
it('removes persist key on submit', async () => {
const order = {
...getDefaultOrder(marketId),
price: '99',
size: '22',
};
const onSubmit = jest.fn();
const { result } = setup(marketId);
await act(async () => {
useOrderStore.setState({
orders: {
[marketId]: order,
},
});
});
await act(async () => {
result.current.handleSubmit(onSubmit)();
});
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0][0]).toEqual(omit(order, 'persist'));
expect(onSubmit.mock.calls[0][0].persist).toBeUndefined();
});
});
@@ -1,71 +0,0 @@
import omit from 'lodash/omit';
import type { OrderObj } from '@vegaprotocol/orders';
import { getDefaultOrder, useOrder } from '@vegaprotocol/orders';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import type { OrderSubmission } from '@vegaprotocol/wallet';
export type OrderFormFields = OrderObj & {
summary: string;
};
/**
* Connects the order store to a react-hook-form instance. Any time a field
* changes in the store the form will be updated so that validation rules
* for those fields are applied
*/
export const useOrderForm = (marketId: string) => {
const [order, update] = useOrder(marketId);
const {
control,
formState: { errors, isSubmitted },
handleSubmit,
setError,
setValue,
clearErrors,
getValues,
} = useForm<OrderFormFields>({
// order can be undefined if there is nothing in the store, it
// will be created but the form still needs some default values
defaultValues: order || getDefaultOrder(marketId),
});
// Keep form fields in sync with the store values,
// inputs are updating the store, fields need updating
// to ensure validation rules are applied
useEffect(() => {
if (!order) return;
const currOrder = getValues();
for (const k in order) {
const key = k as keyof typeof order;
const curr = currOrder[key];
const value = order[key];
if (value !== curr) {
setValue(key, value, {
shouldValidate: isSubmitted, // only apply validation after the form has been submitted and failed
shouldDirty: true,
shouldTouch: true,
});
}
}
}, [order, isSubmitted, getValues, setValue]);
const handleSubmitWrapper = (cb: (o: OrderSubmission) => void) => {
return handleSubmit(() => {
// remove the persist and iceberg key from the order in the store, the wallet will reject
// an order that contains unrecognized additional keys
cb(omit(order, 'persist', 'iceberg'));
});
};
return {
order,
update,
control,
errors,
setError,
clearErrors,
getValues, // returned for test purposes only
handleSubmit: handleSubmitWrapper,
};
};
@@ -1,62 +0,0 @@
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import type { OrderTimeInForce, Side, OrderType } from '@vegaprotocol/types';
import type * as Schema from '@vegaprotocol/types';
export interface StopOrderFormValues {
side: Side;
triggerDirection: Schema.StopOrderTriggerDirection;
triggerType: 'price' | 'trailingPercentOffset';
triggerPrice: string;
triggerTrailingPercentOffset: string;
type: OrderType;
size: string;
timeInForce: OrderTimeInForce;
price?: string;
expire: boolean;
expiryStrategy?: Schema.StopOrderExpiryStrategy;
expiresAt?: string;
}
type StopOrderFormValuesMap = {
[marketId: string]: Partial<StopOrderFormValues> | undefined;
};
type Update = (
marketId: string,
formValues: Partial<StopOrderFormValues>,
persist?: boolean
) => void;
interface Store {
formValues: StopOrderFormValuesMap;
update: Update;
}
export const useStopOrderFormValues = create<Store>()(
persist(
subscribeWithSelector((set) => ({
formValues: {},
update: (marketId, formValues, persist = true) => {
set((state) => {
return {
formValues: {
...state.formValues,
[marketId]: {
...state.formValues[marketId],
...formValues,
},
},
};
});
},
})),
{
name: 'vega_stop_order_store',
}
)
);
@@ -1,28 +0,0 @@
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
export enum DealTicketType {
Limit = 'Limit',
Market = 'Market',
StopLimit = 'StopLimit',
StopMarket = 'StopMarket',
}
export const useDealTicketTypeStore = create<{
set: (marketId: string, type: DealTicketType) => void;
type: Record<string, DealTicketType>;
}>()(
persist(
subscribeWithSelector((set) => ({
type: {},
set: (marketId: string, type: DealTicketType) =>
set((state) => ({
...state,
type: { ...state.type, [marketId]: type },
})),
})),
{
name: 'deal_ticket_type',
}
)
);
@@ -1,12 +1,64 @@
import type {
OrderSubmission,
StopOrderSetup,
StopOrdersSubmission,
} from '@vegaprotocol/wallet';
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
import type { StopOrderFormValues } from '../hooks/use-stop-order-form-values';
import type {
OrderFormValues,
StopOrderFormValues,
} from '../hooks/use-form-values';
import * as Schema from '@vegaprotocol/types';
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
export const mapFormValuesToOrderSubmission = (
order: OrderFormValues,
marketId: string,
decimalPlaces: number,
positionDecimalPlaces: number
): OrderSubmission => ({
marketId: marketId,
type: order.type,
side: order.side,
timeInForce: order.timeInForce,
price:
order.type === Schema.OrderType.TYPE_LIMIT && order.price
? removeDecimal(order.price, decimalPlaces)
: undefined,
size: removeDecimal(order.size, positionDecimalPlaces),
expiresAt:
order.expiresAt &&
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
? toNanoSeconds(order.expiresAt)
: undefined,
postOnly:
order.type === Schema.OrderType.TYPE_MARKET ? false : order.postOnly,
reduceOnly:
order.type === Schema.OrderType.TYPE_LIMIT &&
![
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)
? false
: order.reduceOnly,
icebergOpts:
(order.type === Schema.OrderType.TYPE_MARKET ||
[
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)) &&
order.iceberg &&
order.peakSize &&
order.minimumVisibleSize
? {
peakSize: removeDecimal(order.peakSize, positionDecimalPlaces),
minimumVisibleSize: removeDecimal(
order.minimumVisibleSize,
positionDecimalPlaces
),
}
: undefined,
});
export const mapFormValuesToStopOrdersSubmission = (
data: StopOrderFormValues,
marketId: string,
@@ -15,9 +67,8 @@ export const mapFormValuesToStopOrdersSubmission = (
): StopOrdersSubmission => {
const submission: StopOrdersSubmission = {};
const stopOrderSetup: StopOrderSetup = {
orderSubmission: normalizeOrderSubmission(
orderSubmission: mapFormValuesToOrderSubmission(
{
marketId,
type: data.type,
side: data.side,
size: data.size,
@@ -25,12 +76,16 @@ export const mapFormValuesToStopOrdersSubmission = (
price: data.price,
reduceOnly: true,
},
marketId,
decimalPlaces,
positionDecimalPlaces
),
};
if (data.triggerType === 'price') {
stopOrderSetup.price = removeDecimal(data.triggerPrice, decimalPlaces);
stopOrderSetup.price = removeDecimal(
data.triggerPrice ?? '',
decimalPlaces
);
} else if (data.triggerType === 'trailingPercentOffset') {
stopOrderSetup.trailingPercentOffset = (
Number(data.triggerTrailingPercentOffset) / 100
@@ -0,0 +1,64 @@
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
import * as Schema from '@vegaprotocol/types';
describe('mapFormValuesToOrderSubmission', () => {
it('sets and formats price only for limit orders', () => {
expect(
mapFormValuesToOrderSubmission(
{ price: '100' } as unknown as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).price
).toBeUndefined();
expect(
mapFormValuesToOrderSubmission(
{
price: '100',
type: Schema.OrderType.TYPE_LIMIT,
} as unknown as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).price
).toEqual('10000');
});
it('sets and formats expiresAt only for time in force orders', () => {
expect(
mapFormValuesToOrderSubmission(
{
expiresAt: '2022-01-01T00:00:00.000Z',
} as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).expiresAt
).toBeUndefined();
expect(
mapFormValuesToOrderSubmission(
{
expiresAt: '2022-01-01T00:00:00.000Z',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
} as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).expiresAt
).toEqual('1640995200000000000');
});
it('formats size', () => {
expect(
mapFormValuesToOrderSubmission(
{
size: '100',
} as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).size
).toEqual('1000');
});
});
@@ -371,6 +371,14 @@ function compileEnvVars() {
'NX_TENDERMINT_WEBSOCKET_URL',
process.env['NX_TENDERMINT_WEBSOCKET_URL']
),
CHROME_EXTENSION_URL: windowOrDefault(
'NX_CHROME_EXTENSION_URL',
process.env['NX_CHROME_EXTENSION_URL']
),
MOZILLA_EXTENSION_URL: windowOrDefault(
'NX_MOZILLA_EXTENSION_URL',
process.env['NX_MOZILLA_EXTENSION_URL']
),
};
return env;
+7 -3
View File
@@ -29,6 +29,7 @@ const EmptyLinks: DAppLinks = {
const ExplorerLinks = {
...EmptyLinks,
[Networks.STAGNET1]: 'https://explorer.stagnet1.vega.rocks',
[Networks.TESTNET]: 'https://explorer.fairground.wtf',
[Networks.VALIDATOR_TESTNET]:
'https://explorer.validators-testnet.vega.rocks',
@@ -40,17 +41,18 @@ const ConsoleLinks = {
...EmptyLinks,
[Networks.STAGNET1]: 'https://trading.stagnet1.vega.rocks',
[Networks.TESTNET]: 'https://console.fairground.wtf',
[Networks.MAINNET]: 'https://console.vega.xyz',
[Networks.MAINNET_MIRROR]: 'https://console.mainnet-mirror.vega.rocks/',
[Networks.MAINNET]: 'https://vega.trading',
[Networks.MAINNET_MIRROR]: 'https://console.mainnet-mirror.vega.rocks',
};
const TokenLinks = {
...EmptyLinks,
[Networks.DEVNET]: 'https://dev.governance.vega.xyz',
[Networks.STAGNET1]: 'https://governance.stagnet1.vega.rocks',
[Networks.TESTNET]: 'https://governance.fairground.wtf',
[Networks.VALIDATOR_TESTNET]:
'https://governance.validators-testnet.vega.rocks',
[Networks.MAINNET_MIRROR]: 'https://governance.mainnet-mirror.vega.rocks/',
[Networks.MAINNET_MIRROR]: 'https://governance.mainnet-mirror.vega.rocks',
[Networks.MAINNET]: 'https://governance.vega.xyz',
};
@@ -160,7 +162,9 @@ export const ExternalLinks = {
MARGIN_CREDIT_RISK:
'https://vega.xyz/papers/margins-and-credit-risk.pdf#page=7',
VEGA_WALLET_URL: 'https://vega.xyz/wallet',
VEGA_WALLET_URL_ABOUT: 'https://vega.xyz/wallet/#overview',
VEGA_WALLET_HOSTED_URL: 'https://vega-hosted-wallet.on.fleek.co/',
VEGA_WALLET_BROWSER_LIST: '',
BLOG: 'https://blog.vega.xyz/',
};
@@ -59,6 +59,8 @@ export const envSchema = z
SENTRY_DSN: z.optional(z.string()),
TENDERMINT_URL: z.optional(z.string()),
TENDERMINT_WEBSOCKET_URL: z.optional(z.string()),
CHROME_EXTENSION_URL: z.optional(z.string()),
MOZILLA_EXTENSION_URL: z.optional(z.string()),
})
.refine(
(data) => {
+4 -4
View File
@@ -20,7 +20,7 @@ describe('FillsTable', () => {
positionDecimalPlaces: 5,
tradableInstrument: {
instrument: {
name: 'test market',
code: 'test market',
product: {
settlementAsset: {
decimals: 2,
@@ -71,7 +71,7 @@ describe('FillsTable', () => {
render(<FillsTable partyId={partyId} rowData={[{ ...buyerFill }]} />);
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
buyerFill.market?.tradableInstrument.instrument.name || '',
buyerFill.market?.tradableInstrument.instrument.code || '',
'+3.00',
'1.00 BTC',
'3.00 BTC',
@@ -106,7 +106,7 @@ describe('FillsTable', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
buyerFill.market?.tradableInstrument.instrument.name || '',
buyerFill.market?.tradableInstrument.instrument.code || '',
'-3.00',
'1.00 BTC',
'3.00 BTC',
@@ -141,7 +141,7 @@ describe('FillsTable', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
buyerFill.market?.tradableInstrument.instrument.name || '',
buyerFill.market?.tradableInstrument.instrument.code || '',
'-3.00',
'1.00 BTC',
'3.00 BTC',
+1 -1
View File
@@ -47,7 +47,7 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.name',
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
},
@@ -4,8 +4,10 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
Intent,
KeyValueTable,
KeyValueTableRow,
Lozenge,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
@@ -22,9 +24,11 @@ interface RowProps {
unformatted?: boolean;
assetSymbol?: string;
noBorder?: boolean;
parentValue?: ReactNode;
hasParentData?: boolean;
}
const Row = ({
export const Row = ({
field,
value,
decimalPlaces,
@@ -32,7 +36,14 @@ const Row = ({
unformatted,
assetSymbol = '',
noBorder = true,
parentValue,
hasParentData,
}: RowProps) => {
// Note: we need both 'parentValue' and 'hasParentData' to do a conditional
// check to differentiate between when parentData itself is missing and when
// a specific parentValue is missing. These values are only used when we
// have successor market parent data.
const className = 'text-sm';
const getFormattedValue = (value: ReactNode) => {
@@ -55,6 +66,10 @@ const Row = ({
const formattedValue = getFormattedValue(value);
if (!formattedValue) return null;
const newValueInSuccessorMarket = hasParentData && value && !parentValue;
const valueDiffersFromParentMarket = parentValue && parentValue !== value;
return (
<KeyValueTableRow
key={field}
@@ -63,10 +78,35 @@ const Row = ({
dtClassName={className}
ddClassName={className}
>
<Tooltip description={tooltipMapping[field]} align="start">
<div tabIndex={-1}>{startCase(t(field))}</div>
</Tooltip>
<span style={{ wordBreak: 'break-word' }}>{formattedValue}</span>
<div className="flex items-center gap-3">
<Tooltip description={tooltipMapping[field]} align="start">
<div tabIndex={-1}>{startCase(t(field))}</div>
</Tooltip>
{valueDiffersFromParentMarket && (
<Lozenge className="py-0" variant={Intent.Primary}>
{t('Updated')}
</Lozenge>
)}
{newValueInSuccessorMarket && (
<Lozenge className="py-0" variant={Intent.Primary}>
{t('Added')}
</Lozenge>
)}
</div>
<div style={{ wordBreak: 'break-word' }}>
{valueDiffersFromParentMarket ? (
<div className="flex items-center gap-3">
<span className="line-through">
{getFormattedValue(parentValue)}
</span>
<span>{formattedValue}</span>
</div>
) : (
formattedValue
)}
</div>
</KeyValueTableRow>
);
};
@@ -79,6 +119,7 @@ export interface MarketInfoTableProps {
children?: ReactNode;
assetSymbol?: string;
noBorder?: boolean;
parentData?: Record<string, ReactNode> | null | undefined;
}
export const MarketInfoTable = ({
@@ -89,25 +130,33 @@ export const MarketInfoTable = ({
children,
assetSymbol,
noBorder,
parentData,
}: MarketInfoTableProps) => {
if (!data || typeof data !== 'object') {
return null;
}
const hasParentData = parentData !== undefined;
return (
<>
<KeyValueTable>
{Object.entries(data).map(([key, value]) => (
<Row
key={key}
field={key}
value={value}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
asPercentage={asPercentage}
unformatted={unformatted}
noBorder={noBorder}
/>
))}
<>
{Object.entries(data).map(([key, value]) => (
<Row
key={key}
field={key}
value={value}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
asPercentage={asPercentage}
unformatted={unformatted}
noBorder={noBorder}
parentValue={parentData?.[key]}
hasParentData={hasParentData}
/>
))}
</>
</KeyValueTable>
<div className="flex flex-col gap-2">{children}</div>
</>
@@ -1,3 +1,4 @@
import isEqual from 'lodash/isEqual';
import type { ReactNode } from 'react';
import { Fragment, useState } from 'react';
import { useMemo } from 'react';
@@ -47,11 +48,14 @@ import {
useSuccessorMarketQuery,
} from '../../__generated__';
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
import type { MarketTradingMode } from '@vegaprotocol/types';
import type { Signer } from '@vegaprotocol/types';
import classNames from 'classnames';
import compact from 'lodash/compact';
type MarketInfoProps = {
market: MarketInfo;
parentMarket?: MarketInfo;
children?: ReactNode;
};
@@ -156,21 +160,43 @@ export const InsurancePoolInfoPanel = ({
);
};
export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
const { data: parentData } = useParentMarketIdQuery({
export const KeyDetailsInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const { data: parentMarketIdData } = useParentMarketIdQuery({
variables: {
marketId: market.id,
},
skip: !FLAGS.SUCCESSOR_MARKETS,
});
const { data: successor } = useSuccessorMarketProposalDetailsQuery({
const { data: successorProposalDetails } =
useSuccessorMarketProposalDetailsQuery({
variables: {
proposalId: market.proposal?.id || '',
},
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
});
// The following queries are needed as the parent market could also have been a successor market.
// Note: the parent market is only passed to this component if the successor markets flag is enabled,
// so that check is not needed in the skip.
const { data: grandparentMarketIdData } = useParentMarketIdQuery({
variables: {
proposalId: market.proposal?.id || '',
marketId: parentMarket?.id || '',
},
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
skip: !parentMarket?.id,
});
const { data: parentSuccessorProposalDetails } =
useSuccessorMarketProposalDetailsQuery({
variables: {
proposalId: parentMarket?.proposal?.id || '',
},
skip: !parentMarket?.proposal?.id,
});
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
@@ -181,11 +207,12 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
? {
name: market.tradableInstrument.instrument.name,
marketID: market.id,
parentMarketID: parentData?.market?.parentMarketID || '-',
parentMarketID: parentMarketIdData?.market?.parentMarketID || '-',
insurancePoolFraction:
(successor?.proposal?.terms.change.__typename === 'NewMarket' &&
successor.proposal.terms.change.successorConfiguration
?.insurancePoolFraction) ||
(successorProposalDetails?.proposal?.terms.change.__typename ===
'NewMarket' &&
successorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction) ||
'-',
tradingMode:
market.tradingMode &&
@@ -205,6 +232,28 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
settlementAssetDecimalPlaces: assetDecimals,
}
}
parentData={
parentMarket && {
name: parentMarket?.tradableInstrument?.instrument?.name,
marketID: parentMarket?.id,
parentMarketID: grandparentMarketIdData?.market?.parentMarketID,
insurancePoolFraction:
parentSuccessorProposalDetails?.proposal?.terms.change
.__typename === 'NewMarket' &&
parentSuccessorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction,
tradingMode:
parentMarket?.tradingMode &&
MarketTradingModeMapping[
parentMarket.tradingMode as MarketTradingMode
],
marketDecimalPlaces: parentMarket?.decimalPlaces,
positionDecimalPlaces: parentMarket?.positionDecimalPlaces,
settlementAssetDecimalPlaces:
parentMarket?.tradableInstrument?.instrument?.product
?.settlementAsset?.decimals,
}
}
/>
);
};
@@ -329,7 +378,10 @@ export const SuccessionLineInfoPanel = ({
);
};
export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
export const InstrumentInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => (
<MarketInfoTable
data={{
marketName: market.tradableInstrument.instrument.name,
@@ -337,6 +389,16 @@ export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
productType: market.tradableInstrument.instrument.product.__typename,
quoteName: market.tradableInstrument.instrument.product.quoteName,
}}
parentData={
parentMarket && {
marketName: parentMarket?.tradableInstrument?.instrument?.name,
code: parentMarket?.tradableInstrument?.instrument?.code,
productType:
parentMarket?.tradableInstrument?.instrument?.product?.__typename,
quoteName:
parentMarket?.tradableInstrument?.instrument?.product?.quoteName,
}
}
/>
);
@@ -349,6 +411,7 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market]
);
const { data: asset } = useAssetDataProvider(assetId ?? '');
return asset ? (
<>
@@ -371,54 +434,148 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
);
};
export const MetadataInfoPanel = ({ market }: MarketInfoProps) => (
const getMarketMetadata = (market: MarketInfo) =>
market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
const [key, value] = tag.split(':');
return { [key]: value };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
export const MetadataInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => (
<MarketInfoTable
data={{
expiryDate: getMarketExpiryDateFormatted(
market.tradableInstrument.instrument.metadata.tags
),
...market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
const [key, value] = tag.split(':');
return { [key]: value };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
...(getMarketMetadata(market) || {}),
}}
parentData={
parentMarket && {
expiryDate: getMarketExpiryDateFormatted(
parentMarket.tradableInstrument.instrument.metadata.tags
),
...(getMarketMetadata(parentMarket) || {}),
}
}
/>
);
export const RiskModelInfoPanel = ({ market }: MarketInfoProps) => {
export const RiskModelInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') {
return null;
}
const { tau, riskAversionParameter } = market.tradableInstrument.riskModel;
return <MarketInfoTable data={{ tau, riskAversionParameter }} unformatted />;
let parentData;
if (
parentMarket?.tradableInstrument?.riskModel?.__typename ===
'LogNormalRiskModel'
) {
const {
tau: parentTau,
riskAversionParameter: parentRiskAversionParameter,
} = market.tradableInstrument.riskModel;
parentData = {
tau: parentTau,
riskAversionParameter: parentRiskAversionParameter,
};
}
return (
<MarketInfoTable
data={{ tau, riskAversionParameter }}
parentData={parentData}
unformatted
/>
);
};
export const RiskParametersInfoPanel = ({ market }: MarketInfoProps) => {
if (market.tradableInstrument.riskModel.__typename === 'LogNormalRiskModel') {
export const RiskParametersInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const marketType = market.tradableInstrument.riskModel.__typename;
let data, parentData;
if (marketType === 'LogNormalRiskModel') {
const { r, sigma, mu } = market.tradableInstrument.riskModel.params;
return <MarketInfoTable data={{ r, sigma, mu }} unformatted />;
}
if (market.tradableInstrument.riskModel.__typename === 'SimpleRiskModel') {
data = { r, sigma, mu };
if (
parentMarket?.tradableInstrument?.riskModel.__typename ===
'LogNormalRiskModel'
) {
const parentParams = parentMarket.tradableInstrument.riskModel.params;
parentData = {
r: parentParams.r,
sigma: parentParams.sigma,
mu: parentParams.mu,
};
}
} else if (marketType === 'SimpleRiskModel') {
const { factorLong, factorShort } =
market.tradableInstrument.riskModel.params;
return <MarketInfoTable data={{ factorLong, factorShort }} unformatted />;
data = { factorLong, factorShort };
if (
parentMarket?.tradableInstrument?.riskModel.__typename ===
'SimpleRiskModel'
) {
const parentParams = parentMarket.tradableInstrument.riskModel.params;
parentData = {
factorLong: parentParams.factorLong,
factorShort: parentParams.factorShort,
};
}
}
return null;
if (!data) return null;
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
};
export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => {
export const RiskFactorsInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
if (!market.riskFactors) {
return null;
}
const { short, long } = market.riskFactors;
return <MarketInfoTable data={{ short, long }} unformatted />;
let parentData;
if (parentMarket?.riskFactors) {
const parentShort = parentMarket.riskFactors.short;
const parentLong = parentMarket.riskFactors.long;
parentData = { short: parentShort, long: parentLong };
}
return (
<MarketInfoTable
data={{ short, long }}
parentData={parentData}
unformatted
/>
);
};
export const PriceMonitoringBoundsInfoPanel = ({
market,
triggerIndex,
parentMarket,
}: MarketInfoProps & {
triggerIndex: number;
}) => {
@@ -426,11 +583,35 @@ export const PriceMonitoringBoundsInfoPanel = ({
dataProvider: marketDataProvider,
variables: { marketId: market.id },
});
const { data: parentData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: parentMarket?.id || '' },
skip:
!parentMarket ||
!parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[
triggerIndex
],
});
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
const parentQuoteUnit =
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
const isParentQuoteUnitEqual = quoteUnit === parentQuoteUnit;
const trigger =
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const parentTrigger =
parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const isParentTriggerEqual = isEqual(trigger, parentTrigger);
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
const parentBounds = parentData?.priceMonitoringBounds?.[triggerIndex];
const shouldShowParentData =
isParentQuoteUnitEqual && isParentTriggerEqual && !!parentBounds;
if (!trigger) {
console.error(
`Could not find data for trigger ${triggerIndex} (market id: ${market.id})`
@@ -457,6 +638,14 @@ export const PriceMonitoringBoundsInfoPanel = ({
highestPrice: bounds.maxValidPrice,
lowestPrice: bounds.minValidPrice,
}}
parentData={
shouldShowParentData
? {
highestPrice: parentBounds.maxValidPrice,
lowestPrice: parentBounds.minValidPrice,
}
: undefined
}
decimalPlaces={market.decimalPlaces}
assetSymbol={quoteUnit}
/>
@@ -472,18 +661,31 @@ export const PriceMonitoringBoundsInfoPanel = ({
export const LiquidityMonitoringParametersInfoPanel = ({
market,
}: MarketInfoProps) => (
<MarketInfoTable
data={{
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
timeWindow:
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
scalingFactor:
market.liquidityMonitoringParameters.targetStakeParameters
.scalingFactor,
}}
/>
);
parentMarket,
}: MarketInfoProps) => {
const marketData = {
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
timeWindow:
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
scalingFactor:
market.liquidityMonitoringParameters.targetStakeParameters.scalingFactor,
};
const parentMarketData = parentMarket
? {
triggeringRatio:
parentMarket.liquidityMonitoringParameters.triggeringRatio,
timeWindow:
parentMarket.liquidityMonitoringParameters.targetStakeParameters
.timeWindow,
scalingFactor:
parentMarket.liquidityMonitoringParameters.targetStakeParameters
.scalingFactor,
}
: {};
return <MarketInfoTable data={marketData} parentData={parentMarketData} />;
};
export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
const assetDecimals =
@@ -510,16 +712,61 @@ export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
);
};
export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
export const LiquidityPriceRangeInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
const parentQuoteUnit =
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const parentLiquidityPriceRange = parentMarket
? formatNumberPercentage(
new BigNumber(parentMarket.lpPriceRange).times(100)
)
: null;
const { data } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: market.id },
});
const { data: parentMarketData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: parentMarket?.id || '' },
skip: !parentMarket,
});
let parentData;
if (parentMarket && parentMarketData && quoteUnit === parentQuoteUnit) {
parentData = {
liquidityPriceRange: `${parentLiquidityPriceRange} of mid price`,
lowestPrice:
parentMarketData?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(parentMarket.lpPriceRange)
.times(parentMarketData.midPrice)
.toString(),
parentMarket.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
parentMarketData?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(parentMarket.lpPriceRange)
.times(parentMarketData.midPrice)
.toString(),
parentMarket.decimalPlaces
)} ${quoteUnit}`,
};
}
return (
<>
<p className="text-sm mb-2">
@@ -552,6 +799,7 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
market.decimalPlaces
)} ${quoteUnit}`,
}}
parentData={parentData}
/>
</>
);
@@ -560,8 +808,12 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
export const OracleInfoPanel = ({
market,
type,
parentMarket,
}: MarketInfoProps & { type: 'settlementData' | 'termination' }) => {
// If this is a successor market, this component will only receive parent market
// data if the termination or settlement data is different from the parent.
const product = market.tradableInstrument.instrument.product;
const parentProduct = parentMarket?.tradableInstrument?.instrument?.product;
const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment();
const { data } = useOracleProofs(ORACLE_PROOFS_URL);
@@ -570,12 +822,33 @@ export const OracleInfoPanel = ({
? product.dataSourceSpecForSettlementData.id
: product.dataSourceSpecForTradingTermination.id;
const parentDataSourceSpecId =
type === 'settlementData'
? parentProduct?.dataSourceSpecForSettlementData?.id
: parentProduct?.dataSourceSpecForTradingTermination?.id;
const dataSourceSpec = (
type === 'settlementData'
? product.dataSourceSpecForSettlementData.data
: product.dataSourceSpecForTradingTermination.data
) as DataSourceDefinition;
const parentDataSourceSpec =
type === 'settlementData'
? parentProduct?.dataSourceSpecForSettlementData?.data
: (parentProduct?.dataSourceSpecForTradingTermination
?.data as DataSourceDefinition);
const isParentDataSourceSpecEqual =
parentDataSourceSpec !== undefined &&
dataSourceSpec === parentDataSourceSpec;
const isParentDataSourceSpecIdEqual =
parentDataSourceSpecId !== undefined &&
dataSourceSpecId === parentDataSourceSpecId;
// We'll only provide successor parent data (if it differs) to the
// DataSourceProof component. Having an old external link struck through
// is unlikely to be useful.
return (
<div className="flex flex-col gap-2">
<DataSourceProof
@@ -584,7 +857,14 @@ export const OracleInfoPanel = ({
providers={data}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentData={
isParentDataSourceSpecEqual ? undefined : parentDataSourceSpec
}
parentDataSourceSpecId={
isParentDataSourceSpecIdEqual ? undefined : parentDataSourceSpecId
}
/>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
@@ -606,14 +886,28 @@ export const DataSourceProof = ({
providers,
type,
dataSourceSpecId,
parentData,
parentDataSourceSpecId,
}: {
data: DataSourceDefinition;
providers: Provider[] | undefined;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentData?: DataSourceDefinition;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, we'll only pass parent data to child
// components for comparison if the data differs from the parent market.
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
let parentSigners: Signer[];
if (
parentData &&
parentData.sourceType.__typename === 'DataSourceDefinitionExternal'
) {
parentSigners = parentData.sourceType.sourceType?.signers || [];
}
if (!providers?.length) {
return <NoOracleProof type={type} />;
@@ -622,7 +916,14 @@ export const DataSourceProof = ({
return (
<div className="flex flex-col gap-2">
{signers.map(({ signer }, i) => {
return (
const parentSigner = parentSigners?.find(
({ signer: ParentSigner }) =>
ParentSigner.__typename === signer.__typename
)?.signer;
const isParentSignerEqual = isEqual(signer, parentSigner);
return isParentSignerEqual ? (
<OracleLink
key={i}
providers={providers}
@@ -630,6 +931,16 @@ export const DataSourceProof = ({
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentSigner={parentSigner}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
</div>
@@ -663,18 +974,8 @@ export const DataSourceProof = ({
return <div>{t('Invalid data source')}</div>;
};
const OracleLink = ({
providers,
signer,
type,
dataSourceSpecId,
}: {
providers: Provider[];
signer: SignerKind;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
}) => {
const signerProviders = providers.filter((p) => {
const getSignerProviders = (signer: SignerKind, providers: Provider[]) =>
providers.filter((p) => {
if (signer.__typename === 'PubKey') {
if (
p.oracle.type === 'public_key' &&
@@ -696,19 +997,62 @@ const OracleLink = ({
return false;
});
const OracleLink = ({
providers,
signer,
type,
dataSourceSpecId,
parentSigner,
parentDataSourceSpecId,
}: {
providers: Provider[];
signer: SignerKind;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentSigner?: SignerKind;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const signerProviders = getSignerProviders(signer, providers);
const parentSignerProviders = parentSigner
? getSignerProviders(parentSigner, providers)
: [];
if (!signerProviders.length) {
return <NoOracleProof type={type} />;
}
return (
<div className="mt-2">
{signerProviders.map((provider) => (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
))}
{signerProviders.map((provider) => {
// Making the assumption here that if the provider name is the same,
// that it is the same provider that the parent market used.
const parentProvider = parentSignerProviders.find(
(p) => p.name === provider.name
);
const isParentProviderEqual =
parentProvider !== undefined && isEqual(provider, parentProvider);
// We only want to pass the parent data to the child component if the
// data differs from the parent market.
return isParentProviderEqual ? (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
parentProvider={parentProvider}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
</div>
);
};
@@ -731,13 +1075,18 @@ const NoOracleProof = ({
const OracleProfile = (props: {
provider: Provider;
dataSourceSpecId: string;
parentProvider?: Provider;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const [open, onChange] = useState(false);
return (
<div key={props.provider.name}>
<OracleBasicProfile
provider={props.provider}
onClick={() => onChange(!open)}
parentProvider={props.parentProvider}
/>
<OracleDialog {...props} open={open} onChange={onChange} />
</div>
@@ -5,6 +5,7 @@ import {
ExternalLink,
Icon,
Intent,
Lozenge,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -59,10 +60,12 @@ export const OracleBasicProfile = ({
provider,
onClick,
markets: oracleMarkets,
parentProvider,
}: {
provider: Provider;
markets?: OracleMarketSpecFieldsFragment[] | undefined;
onClick?: (value?: boolean) => void;
parentProvider?: Provider;
}) => {
const { icon, message, intent } = getVerifiedStatusIcon(provider);
@@ -78,8 +81,14 @@ export const OracleBasicProfile = ({
icon: getLinkIcon(proof.type),
}));
// If this is a successor market and there's a different parent provider,
// we'll just show that there's been a change, rather than add old data
// in alongside the new provider.
return (
<>
{parentProvider && (
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
)}
<span className="flex gap-1">
{provider.url && (
<span className="flex align-items-bottom text-md gap-1">
@@ -11,16 +11,27 @@ export const OracleDialog = ({
dataSourceSpecId,
open,
onChange,
parentProvider,
}: {
dataSourceSpecId: string;
provider: Provider;
open: boolean;
onChange?: (isOpen: boolean) => void;
parentProvider?: Provider;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data. We'll pass this on to the title component
// to show a change, but the full profile showing changes is unwieldy - it's enough
// to know from the title that the oracle has changed.
const oracleMarkets = useOracleMarkets(provider);
return (
<Dialog
title={<OracleProfileTitle provider={provider} />}
title={
<OracleProfileTitle
provider={provider}
parentProvider={parentProvider}
/>
}
aria-labelledby="oracle-proof-dialog"
open={open}
onChange={onChange}
@@ -6,6 +6,7 @@ import {
ExternalLink,
Icon,
Intent,
Lozenge,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -18,15 +19,30 @@ import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleM
import ReactMarkdown from 'react-markdown';
import { useState } from 'react';
export const OracleProfileTitle = ({ provider }: { provider: Provider }) => {
export const OracleProfileTitle = ({
provider,
parentProvider,
}: {
provider: Provider;
parentProvider?: Provider;
}) => {
// If this is a successor market, the parent provider will only have been passed
// in if it differs from the current provider. If it is different, we'll just
// show the change in name, not icons and proofs.
const { icon, intent } = getVerifiedStatusIcon(provider);
const verifiedProofs = provider.proofs.filter(
(proof) => proof.available === true
);
return (
<span className="flex gap-1">
{parentProvider && (
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
)}
{provider.url && (
<span>
{parentProvider && parentProvider.name && (
<span className="line-through">{parentProvider.name}</span>
)}
<span className="pr-1">{provider.name}</span>
<span className="dark:text-vega-light-300 text-vega-dark-300">
({verifiedProofs.length})
@@ -150,3 +150,9 @@ query StopOrders($partyId: ID!) {
}
}
}
query StopOrderById($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...StopOrderFields
}
}
@@ -32,7 +32,7 @@ export type OrdersUpdateSubscriptionVariables = Types.Exact<{
export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', id: string, marketId: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null }> | null };
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null};
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null };
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
@@ -43,6 +43,13 @@ export type StopOrdersQueryVariables = Types.Exact<{
export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null };
export type StopOrderByIdQueryVariables = Types.Exact<{
stopOrderId: Types.Scalars['ID'];
}>;
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null };
export const OrderFieldsFragmentDoc = gql`
fragment OrderFields on Order {
id
@@ -309,4 +316,39 @@ export function useStopOrdersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
}
export type StopOrdersQueryHookResult = ReturnType<typeof useStopOrdersQuery>;
export type StopOrdersLazyQueryHookResult = ReturnType<typeof useStopOrdersLazyQuery>;
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
export const StopOrderByIdDocument = gql`
query StopOrderById($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...StopOrderFields
}
}
${StopOrderFieldsFragmentDoc}`;
/**
* __useStopOrderByIdQuery__
*
* To run a query within a React component, call `useStopOrderByIdQuery` and pass it any options that fit your needs.
* When your component renders, `useStopOrderByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useStopOrderByIdQuery({
* variables: {
* stopOrderId: // value for 'stopOrderId'
* },
* });
*/
export function useStopOrderByIdQuery(baseOptions: Apollo.QueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
}
export function useStopOrderByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
}
export type StopOrderByIdQueryHookResult = ReturnType<typeof useStopOrderByIdQuery>;
export type StopOrderByIdLazyQueryHookResult = ReturnType<typeof useStopOrderByIdLazyQuery>;
export type StopOrderByIdQueryResult = Apollo.QueryResult<StopOrderByIdQuery, StopOrderByIdQueryVariables>;
@@ -114,9 +114,11 @@ describe('OrderListTable', () => {
it('should apply correct formatting applied for an iceberg order', async () => {
const icebergOrder = {
...limitOrder,
size: '100',
remaining: '50',
icebergOrder: {
__typename: 'IcebergOrder',
minimumVisibleSize: '100',
minimumVisibleSize: '1',
peakSize: '50',
reservedRemaining: '50',
} as OrderFieldsFragment['icebergOrder'],
@@ -127,8 +129,8 @@ describe('OrderListTable', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues: string[] = [
icebergOrder.market?.tradableInstrument.instrument.code || '',
'0.05',
'0.10',
'0.00',
'+1.00',
Schema.OrderTypeMapping[
icebergOrder.type || Schema.OrderType.TYPE_LIMIT
] + ' (Iceberg)',
@@ -86,21 +86,13 @@ export const OrderListTable = memo<
valueGetter: ({ data }: VegaValueGetterParams<Order>) => {
if (data?.icebergOrder) {
return data?.size && data.market
? toBigNum(
(
BigInt(data.size) -
BigInt(data.remaining) -
BigInt(data.icebergOrder.reservedRemaining)
).toString(),
data.market.positionDecimalPlaces ?? 0
).toNumber()
? BigInt(data.size) -
BigInt(data.remaining) -
BigInt(data.icebergOrder.reservedRemaining)
: undefined;
}
return data?.size && data.market
? toBigNum(
(BigInt(data.size) - BigInt(data.remaining)).toString(),
data.market.positionDecimalPlaces ?? 0
).toNumber()
? BigInt(data.size) - BigInt(data.remaining)
: undefined;
},
valueFormatter: ({
@@ -114,8 +106,8 @@ export const OrderListTable = memo<
return '-';
}
return addDecimalsFormatNumber(
(BigInt(data.size) - BigInt(data.remaining)).toString(),
data.market.positionDecimalPlaces
value,
data.market.positionDecimalPlaces ?? 0
);
},
minWidth: 50,
@@ -18,7 +18,6 @@ import {
import type { Order } from '../order-data-provider';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
interface OrderViewDialogProps {
isOpen: boolean;
@@ -51,21 +50,6 @@ export const OrderViewDialog = ({
)}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-side'}>
<div data-testid={'order-side-label'}>{t('Side')}</div>
<div
data-testid={`order-side-value`}
className={classNames({
// BUY
'text-market-green-600 dark:text-market-green':
order.side === Schema.Side.SIDE_BUY,
// SELL
'text-market-red': order.side === Schema.Side.SIDE_SELL,
})}
>
{Schema.SideMapping[order.side as Schema.Side]}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-type'}>
<div data-testid={'order-type-label'}>{t('Type')}</div>
<div data-testid={`order-type-value`}>
@@ -222,23 +206,6 @@ export const OrderViewDialog = ({
{order.icebergOrder ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
{order.icebergOrder && (
<KeyValueTableRow key={'order-side'} className="ml-4">
<div data-testid={'order-side-label'}>{t('Side')}</div>
<div
data-testid={`order-side-value`}
className={classNames('text-right', {
// BUY
'text-market-green-600 dark:text-market-green':
order.side === Schema.Side.SIDE_BUY,
// SELL
'text-market-red': order.side === Schema.Side.SIDE_SELL,
})}
>
{Schema.SideMapping[order.side as Schema.Side]}
</div>
</KeyValueTableRow>
)}
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-peak-size'}
@@ -256,6 +223,7 @@ export const OrderViewDialog = ({
<div data-testid={`order-iceberg-order-peak-size-value`}>
<Size
value={order.icebergOrder.peakSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
@@ -280,6 +248,7 @@ export const OrderViewDialog = ({
<div data-testid={`order-iceberg-order-minimum-visible-size-value`}>
<Size
value={order.icebergOrder.minimumVisibleSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
@@ -298,6 +267,7 @@ export const OrderViewDialog = ({
<div data-testid={`order-iceberg-order-reserved-remaining-value`}>
<Size
value={order.icebergOrder.reservedRemaining}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
@@ -3,6 +3,7 @@ import {
getDateTimeFormat,
isNumeric,
toBigNum,
formatTrigger,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
@@ -55,29 +56,8 @@ export const StopOrdersTable = memo<
sortable: false,
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
if (data && value?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(
value.price,
data.market.decimalPlaces
)}`;
}
if (data && value?.__typename === 'StopOrderTrailingPercentOffset') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return '-';
},
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
data ? formatTrigger(data, data.market.decimalPlaces) : '',
minWidth: 100,
},
{
-1
View File
@@ -1,4 +1,3 @@
export * from './__generated__/OrdersSubscription';
export * from './use-has-amendable-order';
export * from './use-order-update';
export * from './use-order-store';
@@ -1,122 +0,0 @@
import {
getDefaultOrder,
STORAGE_KEY,
useOrder,
useCreateOrderStore,
} from './use-order-store';
import { act, renderHook } from '@testing-library/react';
import { OrderType } from '@vegaprotocol/types';
jest.mock('zustand');
describe('useCreateOrderStore', () => {
const setup = () => {
const { result } = renderHook(() => useCreateOrderStore());
return renderHook(() => result.current());
};
afterEach(() => {
localStorage.clear();
});
it('has a empty default state', async () => {
const { result } = setup();
expect(result.current).toEqual({
orders: {},
update: expect.any(Function),
});
});
it('can update', () => {
const marketId = 'persisted-market-id';
const expectedOrder = {
...getDefaultOrder(marketId),
type: OrderType.TYPE_LIMIT,
persist: true,
};
const { result } = setup();
act(() => {
result.current.update(marketId, { type: OrderType.TYPE_LIMIT });
});
// order should be stored in memory
expect(result.current.orders).toEqual({
[marketId]: expectedOrder,
});
// order SHOULD also be in localStorage
expect(JSON.parse(localStorage.getItem(STORAGE_KEY) || '')).toEqual({
state: {
orders: {
[marketId]: expectedOrder,
},
},
version: 0,
});
});
it('can update without persisting', () => {
const marketId = 'non-persisted-market-id';
const expectedOrder = {
...getDefaultOrder(marketId),
type: OrderType.TYPE_LIMIT,
persist: false,
};
const { result } = setup();
act(() => {
result.current.update(marketId, { type: OrderType.TYPE_LIMIT }, false);
});
// order should be stored in memory
expect(result.current.orders).toEqual({
[marketId]: expectedOrder,
});
// order should NOT be in localStorage
expect(JSON.parse(localStorage.getItem(STORAGE_KEY) || '')).toEqual({
state: {
orders: {},
},
version: 0,
});
});
});
describe('useOrder', () => {
const setup = (marketId: string) => {
return renderHook(() => useOrder(marketId));
};
afterEach(() => {
localStorage.clear();
});
it('creates a new order if it doesnt exist which is only persisted after editing', () => {
const marketId = 'market-id';
const expectedOrder = {
...getDefaultOrder(marketId),
persist: false,
};
const { result } = setup(marketId);
expect(result.current).toEqual([expectedOrder, expect.any(Function)]);
});
it('only persists an order if edited', () => {
const marketId = 'market-id';
const expectedOrder = {
...getDefaultOrder(marketId),
persist: false,
};
const { result } = setup(marketId);
expect(result.current[0]).toMatchObject({
price: expectedOrder.price,
persist: false,
});
const update = { price: '500' };
act(() => {
result.current[1](update);
});
expect(result.current[0]).toMatchObject({
...update,
persist: true,
});
});
});
@@ -1,139 +0,0 @@
import { OrderTimeInForce, Side } from '@vegaprotocol/types';
import { OrderType } from '@vegaprotocol/types';
import { useCallback, useEffect, useRef } from 'react';
import type { StateCreator, UseBoundStore, Mutate, StoreApi } from 'zustand';
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
export type OrderObj = {
marketId: string;
type: OrderType;
side: Side;
size: string;
timeInForce: OrderTimeInForce;
price?: string;
expiresAt?: string | undefined;
persist: boolean; // key used to determine if order should be kept in localStorage
postOnly?: boolean;
reduceOnly?: boolean;
iceberg?: boolean;
icebergOpts?: {
peakSize: string;
minimumVisibleSize: string;
};
};
type OrderMap = { [marketId: string]: OrderObj | undefined };
type UpdateOrder = (
marketId: string,
order: Partial<OrderObj>,
persist?: boolean
) => void;
interface Store {
orders: OrderMap;
update: UpdateOrder;
}
export const STORAGE_KEY = 'vega_order_store';
const orderStateCreator: StateCreator<Store> = (set) => ({
orders: {},
update: (marketId, order, persist = true) => {
set((state) => {
const curr = state.orders[marketId];
const defaultOrder = getDefaultOrder(marketId);
return {
orders: {
...state.orders,
[marketId]: {
...defaultOrder,
...curr,
...order,
persist,
},
},
};
});
},
});
let store: UseBoundStore<Mutate<StoreApi<Store>, []>> | null = null;
const getOrderStore = () => {
if (!store) {
store = create<Store>()(
persist(subscribeWithSelector(orderStateCreator), {
name: STORAGE_KEY,
partialize: (state) => {
// only store the order in localStorage if user has edited, this avoids
// bloating localStorage if a user just visits the page but does not
// edit the ticket
const partializedOrders: OrderMap = {};
for (const o in state.orders) {
const order = state.orders[o];
if (order && order.persist) {
partializedOrders[order.marketId] = order;
}
}
return {
...state,
orders: partializedOrders,
};
},
})
);
}
return store as UseBoundStore<Mutate<StoreApi<Store>, []>>;
};
export const useCreateOrderStore = () => {
const useOrderStoreRef = useRef(getOrderStore());
return useOrderStoreRef.current;
};
/**
* Retrieves an order from the store for a market and
* creates one if it doesn't already exist
*/
export const useOrder = (marketId: string) => {
const useOrderStoreRef = useCreateOrderStore();
const [order, _update] = useOrderStoreRef((store) => {
return [store.orders[marketId], store.update];
});
const update = useCallback(
(o: Partial<OrderObj>, persist = true) => {
_update(marketId, o, persist);
},
[marketId, _update]
);
// add new order to store if it doesn't exist, but don't
// persist until user has edited
useEffect(() => {
if (!order) {
update(
getDefaultOrder(marketId),
false // don't persist the order
);
}
}, [order, marketId, update]);
return [order, update] as const; // make result a tuple
};
export const getDefaultOrder = (marketId: string): OrderObj => ({
marketId,
type: OrderType.TYPE_LIMIT,
side: Side.SIDE_BUY,
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
size: '0',
price: '0',
expiresAt: undefined,
persist: false,
postOnly: false,
reduceOnly: false,
});
@@ -113,6 +113,7 @@ const marketsData = [
instrument: {
__typename: 'Instrument',
name: 'AAVEDAI Monthly (30 Jun 2022)',
code: 'AAVEDAI.MF21',
product: {
settlementAsset: {
symbol: 'tDAI',
@@ -142,6 +143,7 @@ const marketsData = [
instrument: {
__typename: 'Instrument',
name: 'UNIDAI Monthly (30 Jun 2022)',
code: 'UNIDAI.MF21',
product: {
settlementAsset: {
symbol: 'tDAI',
@@ -183,7 +185,7 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[0].marketId).toEqual(
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
);
expect(metrics[0].marketName).toEqual('AAVEDAI Monthly (30 Jun 2022)');
expect(metrics[0].marketName).toEqual('AAVEDAI.MF21');
expect(metrics[0].marketTradingMode).toEqual(
'TRADING_MODE_MONITORING_AUCTION'
);
@@ -208,7 +210,7 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].marketId).toEqual(
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
);
expect(metrics[1].marketName).toEqual('UNIDAI Monthly (30 Jun 2022)');
expect(metrics[1].marketName).toEqual('UNIDAI.MF21');
expect(metrics[1].marketTradingMode).toEqual('TRADING_MODE_CONTINUOUS');
expect(metrics[1].notional).toEqual('86976200');
expect(metrics[1].openVolume).toEqual('-100');
@@ -121,7 +121,7 @@ export const getMetrics = (
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
marketId: market.id,
marketName: market.tradableInstrument.instrument.name,
marketName: market.tradableInstrument.instrument.code,
marketTradingMode: market.tradingMode,
markPrice: marketData ? marketData.markPrice : undefined,
notional: notional
+310 -266
View File
@@ -41,285 +41,329 @@ const singleRow: Position = {
const singleRowData = [singleRow];
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(
<PositionsTable rowData={[]} isReadOnly={false} />
);
expect(baseElement).toBeTruthy();
});
});
it('render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
describe('Positions', () => {
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(
<PositionsTable rowData={[]} isReadOnly={false} />
);
expect(baseElement).toBeTruthy();
});
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(12);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
'Market',
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
'Margin allocated',
'Realised PNL',
'Unrealised PNL',
'Updated',
]);
});
it('render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
});
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
});
it('Does not fail if the market name does not match the split pattern', async () => {
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
const row = [
Object.assign({}, singleRow, { marketName: breakingMarketName }),
];
await act(async () => {
render(<PositionsTable rowData={row} isReadOnly={false} />);
});
expect(screen.getByText(breakingMarketName)).toBeTruthy();
});
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '-100' }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('1,230.0');
});
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[
{
...singleRow,
marketTradingMode:
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
},
]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('-');
});
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');
});
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
});
it('displays realised and unrealised PNL', async () => {
// pnl cells should be rendered with asset dps
const expectedRealised = addDecimalsFormatNumber(
singleRow.realisedPNL,
singleRow.decimals
);
const expectedUnrealised = addDecimalsFormatNumber(
singleRow.unrealisedPNL,
singleRow.decimals
);
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual(expectedRealised);
expect(cells[10].textContent).toEqual(expectedUnrealised);
});
it('displays close button', async () => {
await act(async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
await act(async () => {
render(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '0' }]}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
});
describe('PNLCell', () => {
const props = {
data: undefined,
valueFormatted: '100',
};
it('renders a dash if no data', () => {
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('renders value if no loss socialisation has occurred', () => {
const props = {
data: {
...singleRow,
lossSocialisationAmount: '0',
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
it('renders value with warning tooltip if loss socialisation occurred', async () => {
const props = {
data: {
...singleRow,
lossSocializationAmount: '500',
decimals: 2,
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(12);
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText('Lifetime loss socialisation deductions: 5.00')
).toBeInTheDocument();
});
});
describe('OpenVolumeCell', () => {
const props = {
data: undefined,
valueFormatted: '100',
};
it('renders a dash if no data', () => {
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText('-')).toBeInTheDocument();
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
'Market',
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
'Margin allocated',
'Realised PNL',
'Unrealised PNL',
'Updated',
]);
});
it('renders value if no status is normal', () => {
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
});
it('Does not fail if the market name does not match the split pattern', async () => {
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
const row = [
Object.assign({}, singleRow, { marketName: breakingMarketName }),
];
await act(async () => {
render(<PositionsTable rowData={row} isReadOnly={false} />);
});
expect(screen.getByText(breakingMarketName)).toBeTruthy();
});
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '-100' }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('1,230.0');
});
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[
{
...singleRow,
marketTradingMode:
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
},
]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('-');
});
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');
});
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
});
it('displays realised and unrealised PNL', async () => {
// pnl cells should be rendered with asset dps
const expectedRealised = addDecimalsFormatNumber(
singleRow.realisedPNL,
singleRow.decimals
);
const expectedUnrealised = addDecimalsFormatNumber(
singleRow.unrealisedPNL,
singleRow.decimals
);
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual(expectedRealised);
expect(cells[10].textContent).toEqual(expectedUnrealised);
});
it('displays close button', async () => {
await act(async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
await act(async () => {
render(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '0' }]}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
});
describe('PNLCell', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
data: undefined,
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
it('renders a dash if no data', () => {
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('renders value if no loss socialisation has occurred', () => {
const props = {
data: {
...singleRow,
lossSocialisationAmount: '0',
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
it('renders value with warning tooltip if loss socialisation occurred', async () => {
const props = {
data: {
...singleRow,
lossSocializationAmount: '500',
decimals: 2,
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'Lifetime loss socialisation deductions: 5.00'
)
).toBeInTheDocument();
expect(
within(tooltip).getByText(
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
)
).toBeInTheDocument();
});
});
it('renders status with warning tooltip if not normal', async () => {
describe('OpenVolumeCell', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
data: undefined,
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
).toBeInTheDocument();
it('renders a dash if no data', () => {
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('renders value if no status is normal', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
it('renders status with warning tooltip if orders were closed', async () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
)
).toBeInTheDocument();
});
it('renders status with warning tooltip if position was closed out', async () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'You did not have enough BTC collateral to meet the maintenance margin requirements for your position, so it was closed by the network.'
)
).toBeInTheDocument();
});
});
});
+47 -15
View File
@@ -478,6 +478,12 @@ export const PNLCell = ({
<p className="mb-2">
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
</p>
<p className="mb-2">
{t(
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
[data.assetSymbol]
)}
</p>
{LOSS_SOCIALIZATION_LINK && (
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
{t('Read more about loss socialisation')}
@@ -499,27 +505,51 @@ export const OpenVolumeCell = ({
return <>-</>;
}
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{valueFormatted}</>;
}
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
let primaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
}
let secondaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
[data.assetSymbol]
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
return (
<WarningCell
showIcon={data.status !== PositionStatus.POSITION_STATUS_UNSPECIFIED}
tooltipContent={
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t('Your position was affected by market conditions')}
</p>
<p className="mb-2">
{t(
'Status: %s',
PositionStatusMapping[
PositionStatus.POSITION_STATUS_ORDERS_CLOSED
]
)}
{t('Status: %s', PositionStatusMapping[data.status])}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
@@ -537,15 +567,17 @@ export const OpenVolumeCell = ({
const WarningCell = ({
children,
tooltipContent,
showIcon = true,
}: {
children: ReactNode;
tooltipContent: ReactNode;
showIcon?: boolean;
}) => {
return (
<Tooltip description={tooltipContent}>
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
<span className="text-black dark:text-white mr-1">
<Icon name="warning-sign" size={3} />
{showIcon && <Icon name="warning-sign" size={3} />}
</span>
<span className="text-ellipsis overflow-hidden">{children}</span>
</div>
+3 -2
View File
@@ -1,6 +1,7 @@
export * from './asset-proposal-notification';
export * from './market-proposal-notification';
export * from './protocol-upgrade-countdown';
export * from './protocol-upgrade-proposal-notification';
export * from './proposals-list';
export * from './protocol-upgrade-countdown';
export * from './protocol-upgrade-in-progress-notification';
export * from './protocol-upgrade-proposal-notification';
export * from './voting-progress';
@@ -0,0 +1,77 @@
import { useProtocolUpgradeProposalLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import {
ExternalLink,
Intent,
NotificationBanner,
SHORT,
} from '@vegaprotocol/ui-toolkit';
import type { StoredNextProtocolUpgradeData } from '../lib';
import {
NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT,
useNextProtocolUpgradeProposal,
} from '../lib';
import { useLocalStorageSnapshot } from '@vegaprotocol/react-helpers';
import { useBlockRising } from '../lib/protocol-upgrade-proposals/use-block-rising';
/**
* A flag determining whether to get the upgrade proposal data from local
* storage if `useNextProtocolUpgradeProposal` fails.
*/
const ALLOW_STORED_PROPOSAL_DATA = true;
export const ProtocolUpgradeInProgressNotification = () => {
const { data, error } = useNextProtocolUpgradeProposal(undefined, true);
const [nextUpgrade] = useLocalStorageSnapshot(
NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT
);
const { blocksRising, block } = useBlockRising();
const detailsLink = useProtocolUpgradeProposalLink();
let vegaReleaseTag: string | undefined;
let upgradeBlockHeight: string | undefined;
if (error && !data && nextUpgrade && ALLOW_STORED_PROPOSAL_DATA) {
try {
const stored = JSON.parse(nextUpgrade) as StoredNextProtocolUpgradeData;
vegaReleaseTag = stored.vegaReleaseTag;
upgradeBlockHeight = stored.upgradeBlockHeight;
} catch {
// no op
}
}
/**
* If upgrade is in progress then none of the nodes should produce blocks,
* same should be with the tendermint block info otherwise it's a network
* issue and not an upgrade. The upgrade usually lasts for couple of minutes.
*
* Once the networks is back then the notification disappears.
*/
const upgradeInProgress =
vegaReleaseTag &&
upgradeBlockHeight &&
!blocksRising &&
block <= Number(upgradeBlockHeight);
if (!upgradeInProgress) return null;
return (
<NotificationBanner intent={Intent.Danger} className={SHORT}>
<div className="uppercase">
{t('The network is being upgraded to %s', vegaReleaseTag)}
</div>
<div>
{t(
'Trading and other network activity has stopped until the upgrade is complete.'
)}{' '}
{vegaReleaseTag && (
<ExternalLink href={detailsLink(vegaReleaseTag)}>
{t('View details')}
</ExternalLink>
)}
</div>
</NotificationBanner>
);
};

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