Compare commits

...
Author SHA1 Message Date
Dariusz Majcherczyk abf23e1e77 test: remove only 2023-06-14 15:30:20 +02:00
Dariusz Majcherczyk e0a2f5bb03 test: update e2e tests for acs - closed, all and proposed markets 2023-06-14 14:07:21 +02:00
m.ray f25a58a3c3 fix(liquidity): update liquidity table row id (#4081) 2023-06-13 14:24:29 -07:00
Mikołaj Młodzikowski 37340b4dc3 feat(ci): modify static content to trigger push 2023-06-13 13:43:54 +02:00
Mikołaj Młodzikowski 17ba3eb351 feat(ci): setup static on s3 (#4075) 2023-06-13 11:29:05 +00:00
Matthew Russell 8f966a4ba6 chore: remove copied dir 2023-06-12 12:38:55 -07:00
Sam Keen cc7f403718 feat(governance): hide proposal details for freeform proposals (#4073) 2023-06-12 12:31:03 +01:00
MaciekandMatthew Russell 9184c2374c fix(markets): back with updating by grid api (#4059)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-06-10 07:49:10 -07:00
MaciekandMatthew Russell 37247a504a fix(trading): update click area in network switcher (#4057)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-06-10 07:48:48 -07:00
Bartłomiej Głownia 2e7a6a6458 chore(trading): cleanup paginated data solution, improve performance (#4036) 2023-06-09 19:02:23 -07:00
Ben bf6c13f523 test(trading): 3502-POSI-e2e-tests (#4064) 2023-06-09 17:37:50 -07:00
Matthew Russell a6f3a08d2f chore(trading,governance,explorer): release v0.20.18-core-0.71.8 2023-06-09 15:50:54 -07:00
Matthew Russell f13e456a29 ci(trading,governance,explorer): remove affected project text manipulation 2023-06-09 10:56:42 -07:00
Sam Keen 5cab040189 fix(governance): temporarily skipped flaky staking tests (#4068) 2023-06-09 16:13:44 +01:00
Maciek e47b868c53 fix(withdraws): clear eth network errors (#4044) 2023-06-09 08:58:16 +02:00
Matthew Russell b6286cd0f3 fix(deal-ticket): make ticket submit button always enabled (#4055) 2023-06-08 22:30:25 -07:00
Matthew Russell f66e976b66 revert: "chore(trading): release v0.20.18-core-0.71.8" 2023-06-08 19:41:24 -07:00
Matthew Russell 866e7232db chore(trading): release v0.20.18-core-0.71.8 2023-06-08 18:22:36 -07:00
Matthew Russellandmaciek 5a8ff90890 fix(ui-toolkit): display of loader in large variant (#4056)
Co-authored-by: maciek <maciek@vegaprotocol.io>
2023-06-08 09:16:51 -07:00
Radosław Szpiech 7b8f654906 test(trading): #3505 add test coverage to 6002-MDET-market-details (#4060) 2023-06-08 15:47:23 +02:00
68 changed files with 1885 additions and 1653 deletions
+14 -6
View File
@@ -136,22 +136,30 @@ jobs:
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
projects="$(echo $projects_e2e | sed 's|-e2e||g')"
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
# tools are only applicable to check previews or deploy from develop to mainnet
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
# tools are only applicable to check previews or deploy from develop to mainnet
echo "Deploying tools on preview"
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
projects+=' "multisig-signer" '
fi
if [[ "${{ github.ref }}" =~ .*develop$ ]]; then
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
# tools are only applicable to check previews or deploy from develop to mainnet
echo "Deploying tools on s3"
projects+=' "multisig-signer" '
fi
if echo "$affected" | grep -q static; then
echo "Static are affected"
echo "Deploying static on s3"
projects+=' "static" '
fi
fi
projects_e2e=${projects_e2e%?}
projects_e2e=[${projects_e2e// /,}]
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
echo PROJECTS=$projects >> $GITHUB_ENV
+4
View File
@@ -74,6 +74,10 @@ jobs:
envName="mainnet"
bucketName="tools.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "static" ]]; then
envName="mainnet"
bucketName="static.vega.xyz"
fi
elif [[ "${{ github.ref }}" =~ .*main$ ]]; then
envName="mainnet"
elif [[ "${{ matrix.app}}" = "trading" ]] && [[ "${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }}" = "true" ]]; then
@@ -63,7 +63,7 @@ context(
vegaWalletSetSpecifiedApprovalAmount('1000');
});
describe('Eth wallet - contains VEGA tokens', function () {
describe.skip('Eth wallet - contains VEGA tokens', function () {
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
@@ -102,7 +102,8 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div>
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' && (
proposal.terms.change.__typename !== 'UpdateMarket' &&
proposal.terms.change.__typename !== 'NewFreeform' && (
<div className="mb-4">
<ProposalTerms data={proposal.terms} />
</div>
+3 -1
View File
@@ -1,3 +1,5 @@
# Static
A static CDN for Vega assets
A static CDN for Vega assets: `static.vega.xyz`
prepare assets by running: `yarn nx build static`
+49 -13
View File
@@ -26,27 +26,27 @@
animation-direction: reverse;
}
.pre-loader .loader-item:first-child {
animation-delay: -50ms;
animation-delay: -0.1s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(2) {
animation-delay: 0.2s;
animation-delay: 0.3s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(3) {
animation-delay: -0.6s;
animation-delay: -0.45s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(4) {
animation-delay: 0.4s;
animation-delay: 1s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(5) {
animation-delay: -0.5s;
animation-delay: -0.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(6) {
animation-delay: 0.3s;
animation-delay: 0.9s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(7) {
@@ -54,11 +54,11 @@
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(8) {
animation-delay: 2s;
animation-delay: 1.6s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(9) {
animation-delay: -0.9s;
animation-delay: -0.45s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(10) {
@@ -66,7 +66,7 @@
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(11) {
animation-delay: -0.55s;
animation-delay: -2.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(12) {
@@ -74,21 +74,57 @@
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(13) {
animation-delay: -0.65s;
animation-delay: -1.95s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(14) {
animation-delay: 0.7s;
animation-delay: 2.8s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(15) {
animation-delay: -3.75s;
animation-delay: -0.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(16) {
animation-delay: 1.6s;
animation-delay: 4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(17) {
animation-delay: -0.85s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(18) {
animation-delay: 1.8s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(19) {
animation-delay: -1.9s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(20) {
animation-delay: 5s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(21) {
animation-delay: -5.25s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(22) {
animation-delay: 4.4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(23) {
animation-delay: -5.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(24) {
animation-delay: 4.8s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(25) {
animation-delay: -5s;
animation-direction: alternate;
}
.pre-loader .loader-item {
animation: flickering 0.4s linear infinite alternate;
}
+58
View File
@@ -0,0 +1,58 @@
.pre-loader {
display: flex;
width: 100%;
min-height: 100vh;
justify-content: center;
align-items: center;
.loader-item {
width: 10px;
height: 10px;
background: black;
}
.pre-loader-center {
align-items: center;
display: flex;
flex-direction: column;
}
.pre-loader-wrapper {
width: 50px;
height: 50px;
display: flex;
flex-wrap: wrap;
}
@for $i from 0 through 25 {
.loader-item:nth-child(#{$i}) {
@if $i % 2 == 0 {
animation-delay: #{$i * 50 * random(5)}ms;
animation-direction: reverse;
} @else {
animation-delay: #{$i * -50 * random(5)}ms;
animation-direction: alternate;
}
}
}
.loader-item {
animation: flickering 0.4s linear alternate infinite;
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
100% {
opacity: 0;
}
}
}
html.dark {
.pre-loader {
.loader-item {
background: white;
}
}
}
@@ -450,3 +450,17 @@ describe('Closed markets', { tags: '@smoke' }, () => {
.should('have.text', 'View on Explorer');
});
});
describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('can see no markets message', () => {
// 6001-MARK-034
cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
});
});
@@ -1,7 +1,10 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types';
const rowSelector =
'[data-testid="tab-all-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -60,7 +63,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
// 6001-MARK-035
cy.get(rowSelector)
.first()
.find('[col-id="tradableInstrument.instrument.code"]')
.find(colInstrumentCode)
.should('have.text', 'SOLUSD');
// 6001-MARK-036
@@ -155,6 +158,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
});
it('able to open and sort full market list - market page', () => {
// 6001-MARK-064
const ExpectedSortedMarkets = [
'AAPL.MF21',
'BTCUSD.MF21',
@@ -167,8 +171,38 @@ describe('markets all table', { tags: '@smoke' }, () => {
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
for (let i = 0; i < ExpectedSortedMarkets.length; i++) {
cy.get(`[row-index=${i}]`)
.find('[col-id="tradableInstrument.instrument.code"]')
.find(colInstrumentCode)
.should('have.text', ExpectedSortedMarkets[i]);
}
});
it('can drag and drop columns', () => {
// 6001-MARK-065
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get(colInstrumentCode)
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
cy.get(colInstrumentCode).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const markets: MarketsQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Markets', markets);
});
cy.mockSubscription();
cy.visit('/#/markets/all');
});
it('can see no markets message', () => {
// 6001-MARK-048
cy.getByTestId('tab-all-markets').should('contain.text', 'No markets');
});
});
@@ -1,15 +1,18 @@
import { MarketTradingModeMapping } from '@vegaprotocol/types';
import { MarketState } from '@vegaprotocol/types';
const marketInfoBtn = 'Info';
const row = 'key-value-table-row';
const marketTitle = 'accordion-title';
const externalLink = 'external-link';
const accordionContent = 'accordion-content';
const blockExplorerLink = 'block-explorer-link';
const dialogClose = 'dialog-close';
const dialogContent = 'dialog-content';
const externalLink = 'external-link';
const githubLink = 'github-link';
const liquidityLink = 'view-liquidity-link';
const marketInfoBtn = 'Info';
const marketTitle = 'accordion-title';
const providerName = 'provider-name';
const oracleBannerStatus = 'oracle-banner-status';
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
const oracleFullProfile = 'oracle-full-profile';
const row = 'key-value-table-row';
const verifiedProofs = 'verified-proofs';
describe('market info is displayed', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -17,12 +20,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
before(() => {
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
undefined,
undefined,
'COMPROMISED'
);
cy.mockTradingPage(MarketState.STATE_ACTIVE);
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
@@ -30,16 +28,8 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
cy.wait('@MarketInfo');
});
it('show oracle banner', () => {
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerDialogTrigger)
.should('contain.text', 'Show more')
.click();
cy.getByTestId(oracleFullProfile).should('exist');
});
it('current fees displayed', () => {
// 6002-MDET-101
cy.getByTestId(marketTitle).contains('Current fees').click();
validateMarketDataRow(0, 'Maker Fee', '0.02%');
validateMarketDataRow(1, 'Infrastructure Fee', '0.05%');
@@ -48,6 +38,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('market price', () => {
// 6002-MDET-102
cy.getByTestId(marketTitle).contains('Market price').click();
validateMarketDataRow(0, 'Mark Price', '46,126.90058');
validateMarketDataRow(1, 'Best Bid Price', '44,126.90058 ');
@@ -56,6 +47,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('market volume displayed', () => {
// 6002-MDET-103
cy.getByTestId(marketTitle).contains('Market volume').click();
validateMarketDataRow(1, 'Open Interest', '-');
validateMarketDataRow(2, 'Best Bid Volume', '1');
@@ -65,11 +57,13 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('insurance pool displayed', () => {
// 6002-MDET-104
cy.getByTestId(marketTitle).contains('Insurance pool').click();
validateMarketDataRow(0, 'Balance', '0');
});
it('key details displayed', () => {
// 6002-MDET-201
cy.getByTestId(marketTitle).contains('Key details').click();
validateMarketDataRow(0, 'Name', 'BTCUSD Monthly (30 Jun 2022)');
@@ -85,6 +79,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('instrument displayed', () => {
// 6002-MDET-202
cy.getByTestId(marketTitle).contains('Instrument').click();
validateMarketDataRow(0, 'Market Name', 'BTCUSD Monthly (30 Jun 2022)');
@@ -93,7 +88,30 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(3, 'Quote Name', 'BTC');
});
it('oracle displayed', () => {
// 6002-MDET-203
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId(accordionContent)
.getByTestId(providerName)
.and('contain', 'Another oracle');
cy.getByTestId(providerName).should('be.visible').click();
cy.getByTestId(dialogContent)
.eq(1)
.within(() => {
cy.getByTestId(blockExplorerLink).contains('Block explorer');
cy.getByTestId(githubLink).contains('Oracle repository');
});
cy.getByTestId(dialogClose).click();
cy.getByTestId(accordionContent)
.getByTestId(verifiedProofs)
.and('contain', '1');
});
it('settlement asset displayed', () => {
// 6002-MDET-206
cy.getByTestId(marketTitle).contains('Settlement asset').click();
cy.window().then((win) => {
cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT');
@@ -112,9 +130,12 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
);
validateMarketDataRow(8, 'Withdrawal threshold', '0.0005');
validateMarketDataRow(9, 'Lifetime limit', '1,230');
validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001');
validateMarketDataRow(11, 'Global reward pool account balance', '0.00002');
});
it('metadata displayed', () => {
// 6002-MDET-207
cy.getByTestId(marketTitle).contains('Metadata').click();
validateMarketDataRow(0, 'Formerly', '076BB86A5AA41E3E');
@@ -125,18 +146,21 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('risk model displayed', () => {
// 6002-MDET-208
cy.getByTestId(marketTitle).contains('Risk model').click();
validateMarketDataRow(0, 'Tau', '0.0001140771161');
validateMarketDataRow(1, 'Risk Aversion Parameter', '0.01');
});
it('risk parameters displayed', () => {
// 6002-MDET-209
cy.getByTestId(marketTitle).contains('Risk parameters').click();
validateMarketDataRow(0, 'R', '0.016');
validateMarketDataRow(1, 'Sigma', '0.3');
});
it('risk factors displayed', () => {
// 6002-MDET-210
cy.getByTestId(marketTitle).contains('Risk factors').click();
validateMarketDataRow(0, 'Short', '0.008571790367285281');
@@ -144,6 +168,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('price monitoring bounds displayed', () => {
// 6002-MDET-211
cy.getByTestId(marketTitle).contains('Price monitoring bounds 1').click();
cy.get('p.col-span-1').contains('99.99999% probability price bounds');
cy.get('p.col-span-1').contains('Within 43,200 seconds');
@@ -152,6 +177,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('liquidity monitoring parameters displayed', () => {
// 6002-MDET-212
cy.getByTestId(marketTitle)
.contains('Liquidity monitoring parameters')
.click();
@@ -162,6 +188,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
});
it('liquidity displayed', () => {
// 6002-MDET-213
cy.getByTestId(marketTitle)
.contains(/Liquidity(?! m)/)
.click();
@@ -169,14 +196,14 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(0, 'Target Stake', '10.00 tBTC');
validateMarketDataRow(1, 'Supplied Stake', '0.01 tBTC');
validateMarketDataRow(2, 'Market Value Proxy', '20.00 tBTC');
cy.getByTestId('view-liquidity-link').should(
cy.getByTestId(liquidityLink).should(
'have.text',
'View liquidity provision table'
);
});
it('liquidity price range displayed', () => {
// 6002-MDET-214
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
@@ -184,29 +211,8 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(2, 'Highest Price', '47,049.438 BTC');
});
it('oracle displayed', () => {
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId(accordionContent)
.getByTestId(providerName)
.and('contain', 'Another oracle');
cy.getByTestId(providerName).should('be.visible').click();
cy.getByTestId('dialog-content')
.eq(1)
.within(() => {
cy.getByTestId('block-explorer-link').contains('Block explorer');
cy.getByTestId('github-link').contains('Oracle repository');
});
cy.getByTestId('dialog-close').click();
cy.getByTestId(accordionContent)
.getByTestId('verified-proofs')
.and('contain', '1');
});
it('proposal displayed', () => {
// 6002-MDET-301
cy.getByTestId(marketTitle).contains('Proposal').click();
cy.getByTestId(accordionContent)
@@ -25,6 +25,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.wait('@MarketsCandles');
});
// 6001-MARK-066
it('can toggle the sidebar', () => {
cy.getByTestId('market-selector').should('be.visible');
cy.getByTestId('sidebar-toggle').click();
@@ -84,7 +85,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
});
});
// 6001-MARK-27
// 6001-MARK-027
it('can use the filter options', () => {
// product type
cy.getByTestId('product-Spot').click();
@@ -94,7 +95,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-29
// 6001-MARK-029
cy.getByTestId(searchInput).clear().type('btc');
cy.getByTestId(list).find('a').should('have.length', 2);
cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21');
@@ -1,17 +1,25 @@
import * as Schema from '@vegaprotocol/types';
const marketSummaryBlock = 'header-summary';
const marketExpiry = 'market-expiry';
const marketPrice = 'market-price';
const marketChange = 'market-change';
const marketVolume = 'market-volume';
const marketMode = 'market-trading-mode';
const marketState = 'market-state';
const marketSettlement = 'market-settlement-asset';
const percentageValue = 'price-change-percentage';
const priceChangeValue = 'price-change';
const expirtyTooltip = 'expiry-tooltip';
const externalLink = 'external-link';
const itemHeader = 'item-header';
const itemValue = 'item-value';
const link = 'link';
const liquidityLink = 'view-liquidity-link';
const liquiditySupplied = 'liquidity-supplied';
const liquiditySuppliedTooltip = 'liquidity-supplied-tooltip';
const marketChange = 'market-change';
const marketExpiry = 'market-expiry';
const marketMode = 'market-trading-mode';
const marketName = 'header-title';
const marketPrice = 'market-price';
const marketSettlement = 'market-settlement-asset';
const marketState = 'market-state';
const marketSummaryBlock = 'header-summary';
const marketVolume = 'market-volume';
const percentageValue = 'price-change-percentage';
const priceChangeValue = 'price-change';
const tradingModeTooltip = 'trading-mode-tooltip';
describe('Market trading page', () => {
before(() => {
@@ -31,10 +39,12 @@ describe('Market trading page', () => {
// 7002-SORD-001
// 7002-SORD-002
it('must display market name', () => {
cy.getByTestId('header-title').should('not.be.empty');
// 6002-MDET-001
cy.getByTestId(marketName).should('not.be.empty');
});
it('must see market expiry', () => {
// 6002-MDET-002
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketExpiry).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Expiry');
@@ -44,6 +54,7 @@ describe('Market trading page', () => {
});
it('must see market price', () => {
// 6002-MDET-003
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketPrice).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Price');
@@ -53,6 +64,7 @@ describe('Market trading page', () => {
});
it('must see market change', () => {
// 6002-MDET-004
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketChange).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Change (24h)');
@@ -63,6 +75,7 @@ describe('Market trading page', () => {
});
it('must see market volume', () => {
// 6002-MDET-005
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketVolume).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Volume (24h)');
@@ -72,16 +85,21 @@ describe('Market trading page', () => {
});
it('must see market mode', () => {
// 6002-MDET-006
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketMode).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Trading mode');
cy.getByTestId(itemValue).should('not.be.empty');
cy.getByTestId(itemValue).should(
'have.text',
'Monitoring auction - liquidity (target not met)'
);
});
});
});
it('must see market state', () => {
//7002-SORD-061
it('must see market status', () => {
// 6002-MDET-007
// 7002-SORD-061
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketState).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Status');
@@ -91,6 +109,7 @@ describe('Market trading page', () => {
});
it('must see market settlement', () => {
// 6002-MDET-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketSettlement).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Settlement asset');
@@ -98,15 +117,12 @@ describe('Market trading page', () => {
});
});
});
it('must see market mode', () => {
it('must see market liquidity supplied', () => {
// 6002-MDET-009
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketMode).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Trading mode');
cy.getByTestId(itemValue).should(
'have.text',
'Monitoring auction - liquidity (target not met)'
);
cy.getByTestId(liquiditySupplied).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
@@ -121,14 +137,14 @@ describe('Market trading page', () => {
.realHover();
});
});
cy.getByTestId('expiry-tooltip')
cy.getByTestId(expirtyTooltip)
.eq(0)
.should(
'contain.text',
'This market expires when triggered by its oracle, not on a set date.'
)
.within(() => {
cy.getByTestId('link')
cy.getByTestId(link)
.should('have.attr', 'href')
.and('include', Cypress.env('EXPLORER_URL'));
});
@@ -154,15 +170,14 @@ describe('Market trading page', () => {
.realHover();
});
});
cy.getByTestId('trading-mode-tooltip')
cy.getByTestId(tradingModeTooltip)
.should(
'contain.text',
'This market is in auction until it reaches sufficient liquidity.'
)
.eq(0)
.within(() => {
cy.getByTestId('external-link')
cy.getByTestId(externalLink)
.should('have.attr', 'href')
.and('include', Cypress.env('TRADING_MODE_LINK'));
@@ -174,5 +189,27 @@ describe('Market trading page', () => {
}
});
});
it('should see liquidity supplied tooltip', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(liquiditySupplied).within(() => {
cy.getByTestId(itemValue).realHover();
});
});
cy.getByTestId(liquiditySuppliedTooltip)
.should('contain.text', 'Supplied stake')
.and('contain.text', 'Target stake')
.first()
.within(() => {
cy.getByTestId(liquidityLink).should(
'have.text',
'View liquidity provision table'
);
cy.getByTestId(externalLink).should(
'have.text',
'Learn about providing liquidity'
);
});
});
});
});
@@ -1,16 +1,16 @@
import { checkSorting } from '@vegaprotocol/cypress';
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see table headers', () => {
@@ -35,10 +35,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
it('renders markets correctly', () => {
// 6001-MARK-049
cy.get(rowSelector)
.first()
.find('[col-id="market"]')
.should('have.text', 'ETHUSD');
cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD');
// 6001-MARK-050
cy.get(rowSelector)
@@ -119,6 +116,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
);
});
it('proposed markets tab should be sorted properly', () => {
// 6001-MARK-062
cy.get('[data-testid="Proposed markets"]').click({ force: true });
const marketColDefault = [
'ETHUSD',
@@ -196,4 +194,31 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
];
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
});
it('can drag and drop columns', () => {
// 6001-MARK-063
cy.get(colMarketId).realMouseDown().realMouseMove(700, 15).realMouseUp();
cy.get(colMarketId).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const proposal: ProposalsListQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ProposalsList', proposal);
});
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see no markets message', () => {
// 6001-MARK-061
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
});
});
@@ -0,0 +1,27 @@
import { MarketState } from '@vegaprotocol/types';
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
const oracleBannerStatus = 'oracle-banner-status';
const oracleFullProfile = 'oracle-full-profile';
describe('oracle information', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
undefined,
undefined,
'COMPROMISED'
);
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('show oracle banner', () => {
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerDialogTrigger)
.should('contain.text', 'Show more')
.click();
cy.getByTestId(oracleFullProfile).should('exist');
});
});
@@ -88,7 +88,8 @@ describe(
.pop()
?.toLowerCase()} and not accepting orders`
);
cy.getByTestId('place-order').should('be.disabled');
// 7002-SORD-060
cy.getByTestId('place-order').should('be.enabled');
});
});
});
@@ -77,7 +77,8 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must warn if order size input has too many digits after the decimal place', function () {
// 7002-SORD-016
cy.getByTestId(orderSizeField).clear().type('1.234');
cy.getByTestId(placeOrderBtn).should('be.disabled');
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
@@ -86,12 +87,13 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must warn if order size is set to 0', function () {
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
});
it('must have total margin available', () => {
// 7001-COLL-011
cy.getByTestId('tab-ticket')
@@ -23,13 +23,14 @@ describe(
});
it('should show an error if your balance is zero', () => {
cy.getByTestId('place-order').should('be.disabled');
// 7002-SORD-060
cy.getByTestId('place-order').should('be.enabled');
// 7002-SORD-003
cy.getByTestId('dealticket-error-message-zero-balance').should(
'have.text',
'You need ' +
'tDAI' +
' in your wallet to trade in this market.See all your collateral.Make a deposit'
' in your wallet to trade in this market. See all your collateral.Make a deposit'
);
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
});
@@ -34,9 +34,9 @@ describe('suspended market validation', { tags: '@regression' }, () => {
it('should show warning for market order', function () {
cy.getByTestId(toggleMarket).click();
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-type').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
@@ -59,7 +59,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
cy.getByTestId(orderTIFDropDown).select(
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-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'
@@ -3,6 +3,18 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock';
// #region consts
const closePosition = 'close-position';
const dialogCloseX = 'dialog-close';
const dialogContent = 'dialog-content';
const dropDownMenu = 'dropdown-menu';
const marketActionsContent = 'market-actions-content';
const positions = 'Positions';
const tabPositions = 'tab-positions';
const toastContent = 'toast-content';
const tooltipContent = 'tooltip-content';
// #endregion
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
@@ -11,8 +23,9 @@ beforeEach(() => {
describe('positions', { tags: '@smoke', testIsolation: true }, () => {
it('renders positions on trading page', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId('Positions').click();
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed();
});
@@ -35,174 +48,313 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
}
aliasGQLQuery(req, 'Positions', positions);
});
cy.visit('/#/portfolio');
cy.getByTestId('Positions').click();
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed(true);
});
describe('renders position among some graphql errors', () => {
it('rows should be displayed despite errors', () => {
const errors = [
{
message:
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['marketsConnection', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const marketData = marketsDataQuery();
const edges = marketData.marketsConnection?.edges.map((market) => {
const replace =
market.node.data?.market.id === 'market-2' ? null : market.node.data;
return { ...market, node: { ...market.node, data: replace } };
});
const overrides = {
...marketData,
marketsConnection: { ...marketData.marketsConnection, edges },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
const emptyCells = [
'notional',
'markPrice',
'currentLeverage',
'averageEntryPrice',
];
cy.getByTestId('tab-positions')
.first()
.within(() => {
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(1)
.within(() => {
emptyCells.forEach((cell) => {
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
});
});
});
});
it('error message should be displayed', () => {
const errors = [
{
message:
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['assets', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const overrides = {
marketsConnection: { edges: [] },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
cy.get('[data-testid="tab-positions"]').contains('no market data');
});
it('Close my position', () => {
visitAndClickPositions();
cy.getByTestId(closePosition).first().click();
// 7004-POSI-010
cy.getByTestId(toastContent).should(
'contain.text',
'Awaiting confirmation'
);
});
describe('sorting by ag-grid columns should work well', () => {
it('sorting by Market', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)'];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
cy.getByTestId('Positions').click();
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
it('sorting by notional', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = [
'276,761.40348',
'46,126.90058',
'1,688.20',
];
const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20'];
cy.getByTestId('Positions').click();
checkSorting(
'notional',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
it('sorting by unrealisedPNL', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['8.95', '-0.22519', '8.95'];
const marketsSortedAsc = ['-0.22519', '8.95', '8.95'];
const marketsSortedDesc = ['8.95', '8.95', '-0.22519'];
cy.getByTestId('Positions').click();
checkSorting(
'unrealisedPNL',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});
cy.get('.ag-center-cols-container [col-id="openVolume"]').each(
($openVolume) => {
cy.wrap($openVolume).invoke('text').should('not.be.empty');
}
);
// includes average entry price, mark price, realised PNL & leverage
cy.getByTestId('flash-cell').each(($prices) => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
});
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
});
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
});
describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('rows should be displayed despite errors', () => {
const errors = [
{
message:
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['marketsConnection', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const marketData = marketsDataQuery();
const edges = marketData.marketsConnection?.edges.map((market) => {
const replace =
market.node.data?.market.id === 'market-2' ? null : market.node.data;
return { ...market, node: { ...market.node, data: replace } };
});
const overrides = {
...marketData,
marketsConnection: { ...marketData.marketsConnection, edges },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
const emptyCells = [
'notional',
'markPrice',
'currentLeverage',
'averageEntryPrice',
];
cy.getByTestId(tabPositions)
.first()
.within(() => {
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(1)
.within(() => {
emptyCells.forEach((cell) => {
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
});
});
});
});
it('error message should be displayed', () => {
const errors = [
{
message:
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['assets', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const overrides = {
marketsConnection: { edges: [] },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
cy.getByTestId(tabPositions).contains('no market data');
});
it('sorting by Market', () => {
visitAndClickPositions();
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'ETHBTC Quarterly (30 Jun 2022)',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
cy.getByTestId(positions).click();
// 7004-POSI-003
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
it('Resize column', () => {
let elementWidth: number;
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(250, 0)
.realMouseUp();
});
// 7004-POSI-006
cy.get(`[col-id="marketName"]`)
.invoke('width')
.should('be.greaterThan', 250);
cy.get(`[col-id="marketName"]`)
.invoke('width')
.then((width) => {
elementWidth = width as number;
})
.then(() => {
let localStorageCopy: Record<string, string>;
cy.window().then((win) => {
localStorageCopy = { ...win.localStorage };
});
cy.reload();
cy.window().then((win) => {
Object.keys(localStorageCopy).forEach((key) => {
win.localStorage.setItem(key, localStorageCopy[key]);
});
});
// 7004-POSI-012
cy.get('[col-id="marketName"]')
.invoke('width')
.should('equal', elementWidth);
});
});
it('Scroll horizontally', () => {
visitAndClickPositions();
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(400, 0)
.realMouseUp();
});
cy.get('[col-id="marketName"]').should('be.visible');
cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({
deltaX: 500,
});
// 7004-POSI-004
cy.get('[col-id="updatedAt"]').should('be.visible');
});
it('Drag and drop columns', () => {
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('[col-id="marketName"]')
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
// 7004-POSI-005
cy.get('[col-id="marketName"]').should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
it('I can see warnings', () => {
visitAndClickPositions();
cy.get('[col-id="openVolume"]').within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
// 7004-POSI-011
cy.getByTestId(tooltipContent).should('be.visible');
});
it('Positive and Negative color change', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
// 7004-POSI-007
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="realisedPNL"]',
'text-vega-green',
'text-vega-pink'
);
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="unrealisedPNL"]',
'text-vega-green',
'text-vega-pink'
);
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="openVolume"]',
'text-vega-green',
'text-vega-pink'
);
});
});
it('View settlement asset', () => {
visitAndClickPositions();
cy.get('[col-id="asset"]').within(() => {
cy.get('button[type="button"]').first().click();
});
// 7004-POSI-008
cy.getByTestId(dialogContent).should('be.visible');
cy.getByTestId(dialogCloseX).click();
cy.getByTestId(dropDownMenu).first().click();
cy.getByTestId(marketActionsContent).click();
// 7004-POSI-009
cy.getByTestId(dialogContent).should('be.visible');
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});
cy.get('.ag-center-cols-container [col-id="openVolume"]').each(
($openVolume) => {
cy.wrap($openVolume).invoke('text').should('not.be.empty');
}
);
// includes average entry price, mark price, realised PNL & leverage
cy.getByTestId('flash-cell').each(($prices) => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
});
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
});
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
function assertPNLColor(
pnlSelector: string,
positiveClass: string,
negativeClass: string
) {
cy.get(pnlSelector).each(($el) => {
const value = parseFloat($el.text());
if (value > 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass);
} else if (value < 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass);
} else if (value == 0) {
cy.wrap($el)
.invoke('attr', 'class')
.should('not.contain', negativeClass, positiveClass);
} else {
throw new Error('Unexpected value');
}
});
}
function visitAndClickPositions() {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
}
+1 -1
View File
@@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.16-core-0.71.5
NX_APP_VERSION=v0.20.18-core-0.71.8
@@ -99,7 +99,7 @@ export const MarketLiquiditySupplied = ({
AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS;
const description = marketId ? (
<section>
<section data-testid="liquidity-supplied-tooltip">
<KeyValueTable>
<KeyValueTableRow>
<span>{t('Supplied stake')}</span>
@@ -38,7 +38,6 @@ type Data = Item[];
type QueryData = {
data: Data;
pageInfo?: PageInfo;
totalCount?: number;
};
type CombinedData = {
@@ -115,7 +114,6 @@ const paginatedSubscribe = makeDataProvider<
first,
append: defaultAppend,
getPageInfo: (r) => r?.pageInfo ?? null,
getTotalCount: (r) => r?.totalCount,
},
});
@@ -373,32 +371,10 @@ describe('data provider', () => {
subscription.unsubscribe();
});
it('fills data with nulls if pagination is enabled', async () => {
const totalCount = 1000;
const data: Item[] = new Array(first).fill(null).map((v, i) => ({
cursor: i.toString(),
node: {
id: i.toString(),
},
}));
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data,
totalCount,
pageInfo: {
hasNextPage: true,
},
});
expect(callback.mock.calls[1][0].data?.length).toBe(totalCount);
subscription.unsubscribe();
});
it('loads requested data blocks and inserts data with total count', async () => {
const totalCount = 1000;
it('loads requested data blocks', async () => {
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
totalCount,
pageInfo: {
hasNextPage: true,
endCursor: '100',
@@ -407,168 +383,25 @@ describe('data provider', () => {
// load next page
subscription.load && subscription.load();
let lastQueryArgs =
const lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
expect(lastQueryArgs?.variables?.['pagination']).toEqual({
after: '100',
first,
});
await resolveQuery({
data: generateData(100),
pageInfo: {
hasNextPage: true,
hasNextPage: false,
endCursor: '200',
},
});
// load page with skip
subscription.load && subscription.load(500, 600);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '200',
first,
skip: 300,
});
await resolveQuery({
data: generateData(500),
pageInfo: {
hasNextPage: true,
endCursor: '600',
},
});
// load in the gap
subscription.load && subscription.load(400, 500);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '200',
first,
skip: 200,
});
await resolveQuery({
data: generateData(400),
pageInfo: {
hasNextPage: true,
endCursor: '500',
},
});
// load page after last block
subscription.load && subscription.load(700, 800);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '600',
first,
skip: 100,
});
await resolveQuery({
data: generateData(700),
pageInfo: {
hasNextPage: true,
endCursor: '800',
},
});
// load last page shorter than expected
subscription.load && subscription.load(950, 1050);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '800',
first,
skip: 150,
});
await resolveQuery({
data: generateData(950, 20),
pageInfo: {
hasNextPage: false,
endCursor: '970',
},
});
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(970);
// load next page when pageInfo.hasNextPage === false
const clientQueryCallsLength = clientQuery.mock.calls.length;
subscription.load && subscription.load();
expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength);
// load last page longer than expected
subscription.load && subscription.load(960, 1000);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '960',
first,
});
await resolveQuery({
data: generateData(960, 40),
pageInfo: {
hasNextPage: true,
endCursor: '1000',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(1000);
subscription.unsubscribe();
});
it('loads requested data blocks and inserts data without totalCount', async () => {
const totalCount = undefined;
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
totalCount,
pageInfo: {
hasNextPage: true,
endCursor: '100',
},
});
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
// load next page
subscription.load && subscription.load();
await resolveQuery({
data: generateData(100),
pageInfo: {
hasNextPage: true,
endCursor: '200',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
// load last page
subscription.load && subscription.load();
await resolveQuery({
data: generateData(200, 50),
pageInfo: {
hasNextPage: false,
endCursor: '250',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(250);
subscription.unsubscribe();
});
it('sets total count when first page has no next page', async () => {
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
pageInfo: {
hasNextPage: false,
endCursor: '100',
},
});
const lastCallbackArgs =
callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(100);
subscription.unsubscribe();
});
@@ -752,7 +585,7 @@ describe('derived data provider', () => {
subscription.load && subscription.load();
const lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
expect(lastQueryArgs?.variables?.['pagination']).toEqual({
after: '100',
first,
});
+28 -85
View File
@@ -27,7 +27,6 @@ export interface UpdateCallback<Data, Delta> {
loading: boolean;
loaded: boolean;
pageInfo: PageInfo | null;
totalCount?: number;
}
): void;
}
@@ -40,9 +39,7 @@ export interface Reload {
(forceReset?: boolean): void;
}
type Pagination = Schema.Pagination & {
skip?: number;
};
type Pagination = Schema.Pagination;
export interface PageInfo {
startCursor?: string;
@@ -83,12 +80,8 @@ export interface Append<Data> {
data: Data | null,
insertionData: Data | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination,
totalCount?: number
): {
data: Data | null;
totalCount?: number;
};
pagination?: Pagination
): Data | null;
}
interface GetData<QueryData, Data, Variables> {
@@ -99,10 +92,6 @@ interface GetPageInfo<QueryData> {
(queryData: QueryData): PageInfo | null;
}
interface GetTotalCount<QueryData> {
(queryData: QueryData): number | undefined;
}
interface GetDelta<SubscriptionData, Delta, Variables> {
(
subscriptionData: SubscriptionData,
@@ -119,44 +108,32 @@ export interface Edge<T extends Node> extends Cursor {
node: T;
}
export function defaultAppend<Data>(
data: Data | null,
insertionData: Data | null,
export function defaultAppend<T extends Cursor>(
data: T[] | null,
insertionData: T[] | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination,
totalCount?: number
pagination?: Pagination
) {
if (data && insertionData && insertionPageInfo) {
if (!(data instanceof Array) || !(insertionData instanceof Array)) {
throw new Error(
'data needs to be instance of Edge[] when using pagination'
'data needs to be instance of Array[] when using pagination'
);
}
if (pagination?.after) {
if (data[data.length - 1].cursor === pagination?.after) {
return [...data, ...insertionData];
}
const cursors = data.map((item) => item && item.cursor);
const startIndex = cursors.lastIndexOf(pagination.after);
if (startIndex !== -1) {
const start = startIndex + 1 + (pagination.skip ?? 0);
const end = start + insertionData.length;
let updatedData = [
...data.slice(0, start),
...insertionData,
...data.slice(end),
];
if (!insertionPageInfo.hasNextPage && end !== (totalCount ?? 0)) {
// adjust totalCount if last page is shorter or longer than expected
totalCount = end;
updatedData = updatedData.slice(0, end);
}
return {
data: updatedData,
// increase totalCount if last page is longer than expected
totalCount: totalCount && Math.max(updatedData.length, totalCount),
};
const start = startIndex + 1;
const updatedData = [...data.slice(0, start), ...insertionData];
return updatedData;
}
}
}
return { data, totalCount };
return data;
}
interface DataProviderParams<
@@ -175,7 +152,6 @@ interface DataProviderParams<
getDelta?: GetDelta<SubscriptionData, Delta, Variables>;
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount?: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
};
@@ -245,7 +221,6 @@ function makeDataProviderInternal<
let client: ApolloClient<object>;
let subscription: Subscription[] | undefined;
let pageInfo: PageInfo | null = null;
let totalCount: number | undefined;
// notify single callback about current state, delta is passes optionally only if notify was invoked onNext
const notify = (
@@ -258,7 +233,6 @@ function makeDataProviderInternal<
loading,
loaded,
pageInfo,
totalCount,
...updateData,
});
};
@@ -301,59 +275,41 @@ function makeDataProviderInternal<
}
});
const load = async (start?: number) => {
const load = async () => {
if (!pagination) {
return Promise.reject();
}
if (!pageInfo?.hasNextPage) {
return null;
}
const paginationVariables: Pagination = {
first: pagination.first,
after: pageInfo?.endCursor,
};
if (start !== undefined && data instanceof Array) {
if (!start) {
paginationVariables.after = undefined;
} else if (data && data[start - 1]) {
paginationVariables.after = (data[start - 1] as Cursor).cursor;
} else {
let skip = 1;
while (!data[start - 1 - skip] && skip <= start) {
skip += 1;
}
paginationVariables.skip = skip;
if (skip === start) {
paginationVariables.after = undefined;
} else {
paginationVariables.after = (data[start - 1 - skip] as Cursor).cursor;
}
if (data) {
const endCursor = (data as Cursor[])[(data as Cursor[]).length - 1]
.cursor;
if (endCursor) {
paginationVariables.after = endCursor;
}
} else if (!pageInfo?.hasNextPage) {
return null;
}
const res = await call(paginationVariables);
const insertionData = getData(res.data, variables);
const insertionPageInfo = pagination.getPageInfo(res.data);
({ data, totalCount } = pagination.append(
data = pagination.append(
data,
insertionData,
insertionPageInfo,
paginationVariables,
totalCount
));
paginationVariables
);
pageInfo = insertionPageInfo;
totalCount =
(pagination.getTotalCount && pagination.getTotalCount(res.data)) ??
totalCount;
notifyAll({ insertionData, isInsert: true });
return insertionData;
};
const setData = (updatedData: Data | null) => {
data = updatedData;
if (totalCount !== undefined && data instanceof Array) {
totalCount = data.length;
}
};
const subscriptionSubscribe = () => {
@@ -400,16 +356,6 @@ function makeDataProviderInternal<
);
}
pageInfo = pagination.getPageInfo(res.data);
if (pageInfo && !pageInfo.hasNextPage) {
totalCount = data.length;
} else {
totalCount =
pagination.getTotalCount && pagination.getTotalCount(res.data);
}
if (data && totalCount && data.length < totalCount) {
data.push(...new Array(totalCount - data.length).fill(null));
}
}
// if there was some updates received from subscription during initial query loading apply them on just received data
if (update && data && updateQueue && updateQueue.length > 0) {
@@ -417,9 +363,6 @@ function makeDataProviderInternal<
const delta = updateQueue.shift();
if (delta) {
setData(update(data, delta, reload, variables));
if (totalCount !== undefined && data instanceof Array) {
totalCount = data.length;
}
}
}
}
@@ -590,7 +533,7 @@ const memoize = <
* @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can reload data provider
* @param getData transforms received query data to format that will be stored in data provider
* @param getDelta transforms delta data to format that will be stored in data provider
* @param pagination pagination related functions { getPageInfo, getTotalCount, append, first }
* @param pagination pagination related functions { getPageInfo, append, first }
* @returns Subscribe<Data, Delta> subscribe function
* @example
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({
+3 -18
View File
@@ -12,23 +12,13 @@ export interface useDataProviderParams<
Variables extends OperationVariables | undefined = undefined
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({
delta,
data,
totalCount,
}: {
delta?: Delta;
data: Data | null;
totalCount?: number;
}) => boolean;
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
insert?: ({
insertionData,
data,
totalCount,
}: {
insertionData?: Data | null;
data: Data | null;
totalCount?: number;
}) => boolean;
variables: Variables;
skipUpdates?: boolean;
@@ -56,7 +46,6 @@ export const useDataProvider = <
}: useDataProviderParams<Data, Delta, Variables>) => {
const client = useApolloClient();
const [data, setData] = useState<Data | null>(null);
const [totalCount, setTotalCount] = useState<number>();
const [loading, setLoading] = useState<boolean>(!skip);
const [error, setError] = useState<Error | undefined>(undefined);
const flushRef = useRef<(() => void) | undefined>(undefined);
@@ -101,7 +90,6 @@ export const useDataProvider = <
error,
loading,
insertionData,
totalCount,
isInsert,
isUpdate,
loaded,
@@ -116,19 +104,18 @@ export const useDataProvider = <
(skipUpdatesRef.current ||
(!skipUpdatesRef.current &&
updateRef.current &&
updateRef.current({ delta, data, totalCount })))
updateRef.current({ delta, data })))
) {
return;
}
if (
isInsert &&
insertRef.current &&
insertRef.current({ insertionData, data, totalCount })
insertRef.current({ insertionData, data })
) {
return;
}
}
setTotalCount(totalCount);
setData(data);
if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data });
@@ -150,7 +137,6 @@ export const useDataProvider = <
useEffect(() => {
setData(null);
setError(undefined);
setTotalCount(undefined);
if (updateRef.current) {
updateRef.current({ data: null });
}
@@ -184,7 +170,6 @@ export const useDataProvider = <
flush,
reload,
load,
totalCount,
};
};
@@ -31,6 +31,7 @@ export const MarginWarning = ({ margin, balance, asset }: Props) => {
text: t(`Deposit ${asset.symbol}`),
action: () => openDepositDialog(asset.id),
dataTestId: 'deal-ticket-deposit-dialog-button',
size: 'sm',
}}
/>
);
@@ -21,10 +21,14 @@ export const ZeroBalanceError = ({
testId="dealticket-error-message-zero-balance"
message={
<>
You need {asset.symbol} in your wallet to trade in this market.
{t(
'You need %s in your wallet to trade in this market. ',
asset.symbol
)}
{onClickCollateral && (
<>
See all your <Link onClick={onClickCollateral}>collateral</Link>.
{t('See all your')}{' '}
<Link onClick={onClickCollateral}>collateral</Link>.
</>
)}
</>
@@ -33,7 +37,7 @@ export const ZeroBalanceError = ({
text: t(`Make a deposit`),
action: () => openDepositDialog(asset.id),
dataTestId: 'deal-ticket-deposit-dialog-button',
size: 'md',
size: 'sm',
}}
/>
);
@@ -1,25 +1,15 @@
import { t } from '@vegaprotocol/i18n';
import type { ButtonVariant } from '@vegaprotocol/ui-toolkit';
import { Button } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface Props {
disabled: boolean;
variant: ButtonVariant;
}
export const DealTicketButton = ({ disabled, variant }: Props) => {
const { pubKey, isReadOnly } = useVegaWallet();
const isDisabled = !pubKey || isReadOnly || disabled;
export const DealTicketButton = ({ variant }: Props) => {
return (
<div className="mb-2">
<Button
variant={variant}
fill
type="submit"
disabled={isDisabled}
data-testid="place-order"
>
<Button variant={variant} fill type="submit" data-testid="place-order">
{t('Place order')}
</Button>
</div>
@@ -42,6 +42,9 @@ describe('DealTicket', () => {
it('should display ticket defaults', () => {
const { container } = render(generateJsx());
// place order button should always be enabled
expect(screen.getByTestId('place-order')).toBeEnabled();
// Assert defaults are used
expect(
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`)
@@ -146,7 +146,7 @@ export const DealTicket = ({
});
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
const orders = activeOrders
? activeOrders.map<OrderInfo>(({ node: order }) => ({
? activeOrders.map<OrderInfo>((order) => ({
isMarketOrder: order.type === OrderType.TYPE_MARKET,
price: order.price,
remaining: order.remaining,
@@ -217,6 +217,8 @@ export const DealTicket = ({
});
return;
}
// No error found above clear the error in case it was active on a previous render
clearErrors('summary');
}, [
marketState,
@@ -480,7 +482,6 @@ export const DealTicket = ({
onClickCollateral={onClickCollateral}
/>
<DealTicketButton
disabled={Object.keys(errors).length >= 1 || isReadOnly}
variant={
order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'
}
@@ -562,7 +563,7 @@ const SummaryMessage = memo(
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'md',
size: 'sm',
}}
/>
</div>
@@ -1,14 +1,14 @@
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
export const validateMarketState = (state: Schema.MarketState) => {
export const validateMarketState = (state: MarketState) => {
if (
[
Schema.MarketState.STATE_SETTLED,
Schema.MarketState.STATE_REJECTED,
Schema.MarketState.STATE_TRADING_TERMINATED,
Schema.MarketState.STATE_CANCELLED,
Schema.MarketState.STATE_CLOSED,
MarketState.STATE_SETTLED,
MarketState.STATE_REJECTED,
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(state)
) {
return t(
@@ -16,7 +16,7 @@ export const validateMarketState = (state: Schema.MarketState) => {
);
}
if (state === Schema.MarketState.STATE_PROPOSED) {
if (state === MarketState.STATE_PROPOSED) {
return t(
`This market is ${marketTranslations(
state
@@ -27,11 +27,11 @@ export const validateMarketState = (state: Schema.MarketState) => {
return true;
};
const marketTranslations = (marketState: Schema.MarketState) => {
const marketTranslations = (marketState: MarketState) => {
switch (marketState) {
case Schema.MarketState.STATE_TRADING_TERMINATED:
case MarketState.STATE_TRADING_TERMINATED:
return t('terminated');
default:
return t(Schema.MarketStateMapping[marketState]).toLowerCase();
return t(MarketStateMapping[marketState]).toLowerCase();
}
};
@@ -1,10 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { MarketTradingMode } from '@vegaprotocol/types';
export const validateMarketTradingMode = (
marketTradingMode: Schema.MarketTradingMode
marketTradingMode: MarketTradingMode
) => {
if (marketTradingMode === Schema.MarketTradingMode.TRADING_MODE_NO_TRADING) {
if (marketTradingMode === MarketTradingMode.TRADING_MODE_NO_TRADING) {
return t('Trading terminated');
}
@@ -13,6 +13,24 @@ import { Networks } from '../../';
jest.mock('../../hooks/use-environment');
describe('Network switcher', () => {
const { location } = window;
let hrefSetSpy: jest.SpyInstance;
beforeEach(() => {
hrefSetSpy = jest.fn();
// @ts-ignore can't set location as optional
delete window.location;
window.location = {} as Location;
Object.defineProperty(window.location, 'href', {
// @ts-ignore set cannot take SpyInstance
set: hrefSetSpy,
});
});
afterEach(() => {
window.location = location;
});
it.each`
network | label
${Networks.CUSTOM} | ${envTriggerMapping[Networks.CUSTOM]}
@@ -49,21 +67,29 @@ describe('Network switcher', () => {
render(<NetworkSwitcher />);
await userEvent.click(screen.getByRole('button'));
let links = screen.getAllByRole('link');
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(links[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]);
expect(links[0]).not.toHaveTextContent(t('current'));
expect(links[1]).not.toHaveTextContent(t('current'));
expect(links[0]).not.toHaveTextContent(t('not available'));
expect(links[1]).not.toHaveTextContent(t('not available'));
expect(links[2]).toHaveTextContent(t('Propose a network parameter change'));
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]);
expect(menuitems[0]).not.toHaveTextContent(t('current'));
expect(menuitems[1]).not.toHaveTextContent(t('current'));
expect(menuitems[0]).not.toHaveTextContent(t('not available'));
expect(menuitems[1]).not.toHaveTextContent(t('not available'));
expect(menuitems[2]).toHaveTextContent(t('Advanced'));
expect(menuitems[0]).toHaveTextContent('Advanced');
const links = screen.getAllByRole('link');
await userEvent.click(links[0]);
expect(hrefSetSpy).toHaveBeenCalledWith(mainnetUrl);
expect(links[0]).toHaveAttribute('href', mainnetUrl);
expect(links[1]).toHaveAttribute('href', testnetUrl);
// re open dropdown as clicking an item will close it
await userEvent.click(screen.getByRole('button'));
links = screen.getAllByRole('link');
await userEvent.click(links[1]);
expect(hrefSetSpy).toHaveBeenCalledWith(testnetUrl);
});
it('displays the correct selected network on the default dropdown view', async () => {
@@ -82,10 +108,10 @@ describe('Network switcher', () => {
await userEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
const links = screen.getAllByRole('link');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('current'));
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(links[0]).toHaveTextContent(t('current'));
});
it('displays the correct selected network on the default dropdown view when it does not have an associated url', async () => {
@@ -103,10 +129,10 @@ describe('Network switcher', () => {
await userEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
const links = screen.getAllByRole('link');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('current'));
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(links[0]).toHaveTextContent(t('current'));
});
it('displays the correct state for a network without url on the default dropdown view', async () => {
@@ -123,44 +149,59 @@ describe('Network switcher', () => {
render(<NetworkSwitcher />);
await userEvent.click(screen.getByRole('button'));
const links = screen.getAllByRole('link');
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('not available'));
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(links[0]).toHaveTextContent(t('not available'));
});
it('displays the advanced view in the correct state', async () => {
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
[Networks.CUSTOM]: undefined,
[Networks.MAINNET]: 'https://main.net',
[Networks.TESTNET]: 'https://test.net',
[Networks.VALIDATOR_TESTNET]: 'https://validator-test.net',
[Networks.DEVNET]: 'https://dev.net',
[Networks.STAGNET1]: 'https://stag1.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.DEVNET,
VEGA_NETWORKS,
}));
it.each([Networks.MAINNET, Networks.TESTNET, Networks.DEVNET])(
'displays the advanced view in the correct state',
async (network) => {
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
[Networks.CUSTOM]: undefined,
[Networks.MAINNET]: 'https://main.net',
[Networks.TESTNET]: 'https://test.net',
[Networks.VALIDATOR_TESTNET]: 'https://validator-test.net',
[Networks.DEVNET]: 'https://dev.net',
[Networks.STAGNET1]: 'https://stag1.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.DEVNET,
VEGA_NETWORKS,
}));
render(<NetworkSwitcher />);
render(<NetworkSwitcher />);
await userEvent.click(screen.getByRole('button'));
await userEvent.click(
screen.getByRole('menuitem', { name: t('Advanced') })
);
await userEvent.click(screen.getByTestId('network-switcher'));
[Networks.MAINNET, Networks.TESTNET, Networks.DEVNET].forEach((network) => {
expect(
screen.getByRole('link', { name: envNameMapping[network] })
).toHaveAttribute('href', VEGA_NETWORKS[network]);
expect(
screen.getByText(envDescriptionMapping[network])
await screen.findByRole('menuitem', { name: t('Advanced') })
).toBeInTheDocument();
});
});
await userEvent.click(
screen.getByRole('menuitem', { name: t('Advanced') })
);
expect(
await screen.findByText(envDescriptionMapping[network])
).toBeInTheDocument();
expect(
screen.getByRole('link', {
name: new RegExp(`^${envNameMapping[network]}`),
})
).toBeInTheDocument();
await userEvent.click(
screen.getByRole('link', {
name: new RegExp(`^${envNameMapping[network]}`),
})
);
expect(hrefSetSpy).toHaveBeenCalledWith(VEGA_NETWORKS[network]);
}
);
it('labels the selected network in the advanced view', async () => {
const selectedNetwork = Networks.DEVNET;
@@ -188,7 +229,7 @@ describe('Network switcher', () => {
const label = screen.getByText(`(${t('current')})`);
expect(label).toBeInTheDocument();
expect(label.parentNode?.firstElementChild).toHaveTextContent(
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
envNameMapping[selectedNetwork]
);
});
@@ -217,7 +258,7 @@ describe('Network switcher', () => {
const label = screen.getByText('(not available)');
expect(label).toBeInTheDocument();
expect(label.parentNode?.firstElementChild).toHaveTextContent(
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
envNameMapping[Networks.MAINNET]
);
});
@@ -1,7 +1,6 @@
import { useState, useCallback, useRef } from 'react';
import { useState, useCallback } from 'react';
import { t } from '@vegaprotocol/i18n';
import {
Link,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -99,7 +98,6 @@ export const NetworkSwitcher = ({
},
[setOpen, setAdvancedView]
);
const menuRef = useRef<HTMLButtonElement | null>(null);
const current = currentNetwork || VEGA_ENV;
@@ -110,7 +108,6 @@ export const NetworkSwitcher = ({
trigger={
<DropdownMenuTrigger
data-testid="network-switcher"
ref={menuRef}
className={classNames(
'flex justify-between items-center text-sm text-vega-dark-600 dark:text-vega-light-600 py-1 px-2 rounded border border-vega-dark-200 whitespace-nowrap dark:hover:bg-vega-dark-500 hover:bg-vega-light-500',
className
@@ -123,10 +120,7 @@ export const NetworkSwitcher = ({
</DropdownMenuTrigger>
}
>
<DropdownMenuContent
align="start"
style={{ minWidth: `${menuRef.current?.offsetWidth || 290}px` }}
>
<DropdownMenuContent align="start">
{!isAdvancedView && (
<>
{standardNetworkKeys.map((key) => (
@@ -134,14 +128,16 @@ export const NetworkSwitcher = ({
key={key}
data-testid="network-item"
disabled={!VEGA_NETWORKS[key]}
role="link"
onClick={() =>
(window.location.href = VEGA_NETWORKS[key] || '')
}
>
<a href={VEGA_NETWORKS[key]}>
{envNameMapping[key]}
<NetworkLabel
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</a>
{envNameMapping[key]}
<NetworkLabel
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</DropdownMenuItem>
))}
<DropdownMenuItem
@@ -159,10 +155,17 @@ export const NetworkSwitcher = ({
{isAdvancedView && (
<>
{advancedNetworkKeys.map((key) => (
<DropdownMenuItem key={key} data-testid="network-item-advanced">
<DropdownMenuItem
key={key}
data-testid="network-item-advanced"
role="link"
onClick={() =>
(window.location.href = VEGA_NETWORKS[key] || '')
}
>
<div className="w-full flex justify-between gap-2">
<div>
<Link href={VEGA_NETWORKS[key]}>{envNameMapping[key]}</Link>
{envNameMapping[key]}
<NetworkLabel
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
-1
View File
@@ -1,4 +1,3 @@
export * from './lib/fills-container';
export * from './lib/use-fills-list';
export * from './lib/fills-data-provider';
export * from './lib/__generated__/Fills';
+25 -21
View File
@@ -48,28 +48,32 @@ query Fills($filter: TradesFilter, $pagination: Pagination) {
}
}
fragment FillUpdateFields on TradeUpdate {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
}
subscription FillsEvent($filter: TradesSubscriptionFilter!) {
tradesStream(filter: $filter) {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
...FillUpdateFields
}
}
+29 -22
View File
@@ -15,6 +15,8 @@ export type FillsQueryVariables = Types.Exact<{
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
export type FillsEventSubscriptionVariables = Types.Exact<{
filter: Types.TradesSubscriptionFilter;
}>;
@@ -60,6 +62,31 @@ export const FillEdgeFragmentDoc = gql`
cursor
}
${FillFieldsFragmentDoc}`;
export const FillUpdateFieldsFragmentDoc = gql`
fragment FillUpdateFields on TradeUpdate {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
}
`;
export const FillsDocument = gql`
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
@@ -107,30 +134,10 @@ export type FillsQueryResult = Apollo.QueryResult<FillsQuery, FillsQueryVariable
export const FillsEventDocument = gql`
subscription FillsEvent($filter: TradesSubscriptionFilter!) {
tradesStream(filter: $filter) {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
...FillUpdateFields
}
}
`;
${FillUpdateFieldsFragmentDoc}`;
/**
* __useFillsEventSubscription__
+90 -70
View File
@@ -1,74 +1,34 @@
import produce from 'immer';
import orderBy from 'lodash/orderBy';
import {} from '@vegaprotocol/utils';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import type { PageInfo, Cursor } from '@vegaprotocol/data-provider';
import {
makeDataProvider,
makeDerivedDataProvider,
defaultAppend as append,
paginatedCombineDelta as combineDelta,
paginatedCombineInsertionData as combineInsertionData,
} from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets';
import { FillsDocument, FillsEventDocument } from './__generated__/Fills';
import type {
FillsQuery,
FillsQueryVariables,
FillFieldsFragment,
FillEdgeFragment,
FillsEventSubscription,
FillUpdateFieldsFragment,
FillsEventSubscriptionVariables,
} from './__generated__/Fills';
const update = (
data: FillEdgeFragment[] | null,
delta: FillsEventSubscription['tradesStream']
) => {
return produce(data, (draft) => {
orderBy(delta, 'createdAt').forEach((node) => {
if (draft === null) {
return;
}
const index = draft.findIndex((edge) => edge?.node.id === node.id);
if (index !== -1) {
if (draft[index]?.node) {
Object.assign(draft[index]?.node as FillFieldsFragment, node);
}
} else {
const firstNode = draft[0]?.node;
if (
(firstNode && node.createdAt >= firstNode.createdAt) ||
!firstNode
) {
const { buyerId, sellerId, marketId, ...trade } = node;
draft.unshift({
node: {
...trade,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
},
cursor: '',
__typename: 'TradeEdge',
});
}
}
});
});
};
export type Trade = Omit<FillFieldsFragment, 'market'> & {
market?: Market;
isLastPlaceholder?: boolean;
};
export type TradeEdge = Edge<Trade>;
const getData = (responseData: FillsQuery | null): FillEdgeFragment[] =>
responseData?.trades?.edges || [];
const getData = (
responseData: FillsQuery | null
): (FillFieldsFragment & Cursor)[] =>
responseData?.trades?.edges.map<FillFieldsFragment & Cursor>((edge) => ({
...edge.node,
cursor: edge.cursor,
})) || [];
const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
responseData?.trades?.pageInfo || null;
@@ -76,16 +36,65 @@ const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
const getDelta = (subscriptionData: FillsEventSubscription) =>
subscriptionData.tradesStream || [];
const mapFillUpdateToFill = (
fillUpdate: FillUpdateFieldsFragment
): FillFieldsFragment => {
const { buyerId, sellerId, marketId, ...fill } = fillUpdate;
return {
...fill,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
};
};
const mapFillUpdateToFillWithMarket =
(markets: Record<string, Market>) =>
(fillUpdate: FillUpdateFieldsFragment): Trade => {
const { market, ...fill } = mapFillUpdateToFill(fillUpdate);
return {
...fill,
market: markets[market.id],
};
};
const update = <T extends Omit<FillFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
variables: FillsQueryVariables,
mapDeltaToData: (delta: FillUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
orderBy(delta, 'createdAt', 'desc').forEach((fillUpdate) => {
const index = data?.findIndex((fill) => fill.id === fillUpdate.id) ?? -1;
if (index !== -1) {
updatedData[index] = {
...updatedData[index],
...mapDeltaToData(fillUpdate),
};
} else if (!data?.length || fillUpdate.createdAt >= data[0].createdAt) {
updatedData.unshift(mapDeltaToData(fillUpdate));
}
});
return updatedData;
};
export const fillsProvider = makeDataProvider<
Parameters<typeof getData>['0'],
ReturnType<typeof getData>,
Parameters<typeof getDelta>['0'],
ReturnType<typeof getDelta>,
FillsQueryVariables
FillsQueryVariables,
FillsEventSubscriptionVariables
>({
query: FillsDocument,
subscriptionQuery: FillsEventDocument,
update,
update: (data, delta, reload, variables) =>
update(data, delta, variables, mapFillUpdateToFill),
getData,
getDelta,
pagination: {
@@ -93,30 +102,41 @@ export const fillsProvider = makeDataProvider<
append,
first: 100,
},
getSubscriptionVariables: ({ filter }) => {
const variables: FillsEventSubscriptionVariables = { filter: {} };
if (filter) {
variables.filter = {
partyIds: filter.partyIds,
marketIds: filter.marketIds,
};
}
return variables;
},
});
export const fillsWithMarketProvider = makeDerivedDataProvider<
(TradeEdge | null)[],
Trade[],
never,
FillsQueryVariables
>(
[
fillsProvider,
(callback, client) => marketsProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(partsData): (TradeEdge | null)[] =>
(partsData[0] as ReturnType<typeof getData>)?.map(
(edge) =>
edge && {
cursor: edge.cursor,
node: {
...edge.node,
market: (partsData[1] as Market[]).find(
(market) => market.id === edge.node.market.id
),
},
}
) || null,
combineDelta<Trade, ReturnType<typeof getDelta>['0']>,
combineInsertionData<Trade>
(partsData, variables, prevData, parts): Trade[] | null => {
if (prevData && parts[0].isUpdate) {
return update(
prevData,
parts[0].delta as ReturnType<typeof getDelta>,
variables,
mapFillUpdateToFillWithMarket(partsData[1] as Record<string, Market>)
);
}
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
(trade) => ({
...trade,
market: (partsData[1] as Record<string, Market>)[trade.market.id],
})
);
}
);
+28 -22
View File
@@ -1,10 +1,11 @@
import compact from 'lodash/compact';
import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table';
import { useFillsList } from './use-fills-list';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import { fillsWithMarketProvider } from './fills-data-provider';
interface FillsManagerProps {
partyId: string;
@@ -20,31 +21,36 @@ export const FillsManager = ({
storeKey,
}: FillsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const scrolledToTop = useRef(true);
const { data, error } = useFillsList({
partyId,
marketId,
gridRef,
scrolledToTop,
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
partyIds: [partyId],
};
if (marketId) {
filter.marketIds = [marketId];
}
const { data, error } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update: ({ data }) => {
if (data?.length && gridRef.current?.api) {
gridRef.current?.api.setRowData(data);
return true;
}
return false;
},
variables: { filter },
});
const bottomPlaceholderProps = useBottomPlaceholder({
gridRef,
});
const fills = compact(data).map((e) => e.node);
return (
<div className="h-full relative">
<FillsTable
ref={gridRef}
rowData={fills}
partyId={partyId}
onMarketClick={onMarketClick}
storeKey={storeKey}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
/>
</div>
<FillsTable
ref={gridRef}
rowData={data}
partyId={partyId}
onMarketClick={onMarketClick}
storeKey={storeKey}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
/>
);
};
-95
View File
@@ -1,95 +0,0 @@
import type { AgGridReact } from 'ag-grid-react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { useFillsList } from './use-fills-list';
import type { TradeEdge } from './fills-data-provider';
let mockData = null;
let mockDataProviderData = {
data: mockData as (TradeEdge | null)[] | null,
error: undefined,
loading: true,
};
let updateMock: jest.Mock;
const mockDataProvider = jest.fn((args) => {
updateMock = args.update;
return mockDataProviderData;
});
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn((args) => mockDataProvider(args)),
}));
describe('useFillsList Hook', () => {
const mockRefreshAgGridApi = jest.fn();
const partyId = 'partyId';
const gridRef = {
current: {
api: {
refreshInfiniteCache: mockRefreshAgGridApi,
getModel: () => ({ getType: () => 'infinite' }),
},
} as unknown as AgGridReact,
};
const scrolledToTop = {
current: false,
};
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper dataProvider results', () => {
const { result } = renderHook(
() => useFillsList({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: null,
error: undefined,
loading: true,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
});
it('return proper mocked results', () => {
mockData = [
{
node: {
id: 'data_id_1',
},
} as unknown as TradeEdge,
{
node: {
id: 'data_id_2',
},
} as unknown as TradeEdge,
];
mockDataProviderData = {
...mockDataProviderData,
data: mockData,
loading: false,
};
const { result } = renderHook(
() => useFillsList({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: mockData,
error: undefined,
loading: false,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
updateMock({ data: mockData });
expect(mockRefreshAgGridApi).not.toHaveBeenCalled();
updateMock({ data: mockData });
expect(mockRefreshAgGridApi).toHaveBeenCalled();
});
});
-123
View File
@@ -1,123 +0,0 @@
import type { RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef } from 'react';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types';
import { updateGridData } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Trade, TradeEdge } from './fills-data-provider';
import { fillsWithMarketProvider } from './fills-data-provider';
interface Props {
partyId: string;
marketId?: string;
gridRef: RefObject<AgGridReact>;
scrolledToTop: RefObject<boolean>;
}
export const useFillsList = ({
partyId,
marketId,
gridRef,
scrolledToTop,
}: Props) => {
const dataRef = useRef<(TradeEdge | null)[] | null>(null);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const placeholderAdded = useRef(-1);
const makeBottomPlaceholders = useCallback((trade?: Trade) => {
if (!trade) {
if (placeholderAdded.current >= 0) {
dataRef.current?.splice(placeholderAdded.current, 1);
}
placeholderAdded.current = -1;
} else if (placeholderAdded.current === -1) {
dataRef.current?.push({
node: { ...trade, id: `${trade?.id}-1`, isLastPlaceholder: true },
});
placeholderAdded.current = (dataRef.current?.length || 0) - 1;
}
}, []);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
gridRef.current?.api?.refreshInfiniteCache();
}, [gridRef]);
const update = useCallback(
({
data,
delta,
}: {
data: (TradeEdge | null)[] | null;
delta?: Trade[];
}) => {
if (dataRef.current?.length) {
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += (delta || []).filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
return updateGridData(dataRef, data, gridRef);
}
dataRef.current = data;
return false;
},
[gridRef, scrolledToTop]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: (TradeEdge | null)[] | null;
totalCount?: number;
}) => {
totalCountRef.current = totalCount;
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const filter: Types.TradesFilter & Types.TradesSubscriptionFilter = {
partyIds: [partyId],
};
if (marketId) {
filter.marketIds = [marketId];
}
const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update,
insert,
variables: { filter },
});
totalCountRef.current = totalCount;
const getRows = makeInfiniteScrollGetRows<TradeEdge>(
dataRef,
totalCountRef,
load,
newRows
);
return {
data,
error,
loading,
addNewRows,
getRows,
reload,
makeBottomPlaceholders,
};
};
@@ -1,22 +1,12 @@
import type { Asset } from '@vegaprotocol/assets';
import { assetsProvider } from '@vegaprotocol/assets';
import { assetsMapProvider } from '@vegaprotocol/assets';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
import { updateGridData } from '@vegaprotocol/datagrid';
import { marketsMapProvider } from '@vegaprotocol/markets';
import {
makeDataProvider,
makeDerivedDataProvider,
useDataProvider,
} from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import type { AgGridReact } from 'ag-grid-react';
import produce from 'immer';
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';
import type { RefObject } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import type { Filter } from './ledger-manager';
import type {
LedgerEntriesQuery,
LedgerEntriesQueryVariables,
@@ -30,171 +20,58 @@ export type LedgerEntry = LedgerEntryFragment & {
marketReceiver: Market | null | undefined;
};
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
export type AggregatedLedgerEntriesNode = Omit<
AggregatedLedgerEntriesEdge,
'node'
> & {
node: LedgerEntry;
};
type Edge = LedgerEntriesQuery['ledgerEntries']['edges'][number];
const isLedgerEntryEdge = (entry: Edge): entry is NonNullable<Edge> =>
entry !== null;
const getData = (responseData: LedgerEntriesQuery | null) => {
return responseData?.ledgerEntries?.edges || [];
return (
responseData?.ledgerEntries?.edges
.filter(isLedgerEntryEdge)
.map((edge) => edge.node) || []
);
};
export const update = (
data: ReturnType<typeof getData> | null,
delta: ReturnType<typeof getData>,
reload: () => void,
variables: LedgerEntriesQueryVariables
) => {
if (!data) {
return data;
}
return produce(data, (draft) => {
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
const incoming = uniqBy(
orderBy(delta, (entry) => entry?.node.vegaTime, 'desc'),
'id'
);
// Add or update incoming orders
incoming.reverse().forEach((node) => {
const index = draft.findIndex(
(edge) => edge?.node.vegaTime === node?.node.vegaTime
);
const newer =
draft.length === 0 || node?.node.vegaTime >= draft[0]?.node.vegaTime;
let doesFilterPass = true;
if (
doesFilterPass &&
variables?.dateRange?.start &&
new Date(node?.node.vegaTime) <= new Date(variables?.dateRange?.start)
) {
doesFilterPass = false;
}
if (
doesFilterPass &&
variables?.dateRange?.end &&
new Date(node?.node.vegaTime) >= new Date(variables?.dateRange?.end)
) {
doesFilterPass = false;
}
if (index !== -1) {
if (doesFilterPass) {
// Object.assign(draft[index]?.node, node?.node);
if (newer) {
draft.unshift(...draft.splice(index, 1));
}
} else {
draft.splice(index, 1);
}
} else if (newer && doesFilterPass) {
draft.unshift(node);
}
});
});
};
const ledgerEntriesOnlyProvider = makeDataProvider({
const ledgerEntriesOnlyProvider = makeDataProvider<
LedgerEntriesQuery,
ReturnType<typeof getData>,
never,
never,
LedgerEntriesQueryVariables
>({
query: LedgerEntriesDocument,
getData,
getDelta: getData,
update,
additionalContext: {
isEnlargedTimeout: true,
},
});
export const ledgerEntriesProvider = makeDerivedDataProvider<
AggregatedLedgerEntriesNode[],
AggregatedLedgerEntriesNode[],
LedgerEntry[],
never,
LedgerEntriesQueryVariables
>(
[
ledgerEntriesOnlyProvider,
(callback, client) => assetsProvider(callback, client, undefined),
(callback, client) => marketsProvider(callback, client, undefined),
(callback, client) => assetsMapProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
([entries, assets, markets]) => {
return entries.map((edge: AggregatedLedgerEntriesEdge) => {
const entry = edge.node;
const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
const marketSender = markets.find(
(market: Market) => market.id === entry.fromAccountMarketId
);
const marketReceiver = markets.find(
(market: Market) => market.id === entry.toAccountMarketId
);
const cursor = edge?.cursor;
return {
node: { ...entry, asset, marketSender, marketReceiver },
cursor,
};
(partsData) => {
const entries = partsData[0] as ReturnType<typeof getData>;
const assets = partsData[1] as Record<string, Asset>;
const markets = partsData[1] as Record<string, Market>;
return entries.map((entry) => {
const asset = entry.assetId
? (assets as Record<string, Asset>)[entry.assetId]
: null;
const marketSender = entry.fromAccountMarketId
? markets[entry.fromAccountMarketId]
: null;
const marketReceiver = entry.toAccountMarketId
? markets[entry.toAccountMarketId]
: null;
return { ...entry, asset, marketSender, marketReceiver };
});
}
);
interface Props {
partyId: string;
filter?: Filter;
gridRef: RefObject<AgGridReact>;
}
export const useLedgerEntriesDataProvider = ({
partyId,
filter,
gridRef,
}: Props) => {
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
const totalCountRef = useRef<number>();
const variables = useMemo<LedgerEntriesQueryVariables>(
() => ({
partyId,
dateRange: filter?.vegaTime?.value,
pagination: {
first: 5000,
},
}),
[partyId, filter?.vegaTime?.value]
);
const update = useCallback(
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: AggregatedLedgerEntriesEdge[] | null;
totalCount?: number;
}) => {
totalCountRef.current = totalCount;
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: ledgerEntriesProvider,
update,
insert,
variables,
skip: !variables.partyId,
});
totalCountRef.current = totalCount;
const getRows = makeInfiniteScrollGetRows<AggregatedLedgerEntriesEdge>(
dataRef,
totalCountRef,
load
);
return { loading, error, data, getRows, reload };
};
+21 -13
View File
@@ -2,10 +2,12 @@ import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useRef, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns';
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
import { LedgerTable } from './ledger-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types';
import { LedgerExportLink } from './ledger-export-link';
@@ -26,10 +28,21 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter>(defaultFilter);
const { data, error } = useLedgerEntriesDataProvider({
partyId,
filter,
gridRef,
const variables = useMemo<LedgerEntriesQueryVariables>(
() => ({
partyId,
dateRange: filter?.vegaTime?.value,
pagination: {
first: 5000,
},
}),
[partyId, filter?.vegaTime?.value]
);
const { data, error } = useDataProvider({
dataProvider: ledgerEntriesProvider,
variables,
skip: !variables.partyId,
});
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
@@ -37,20 +50,15 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
setFilter(updatedFilter);
}, []);
// allow passing undefined to grid so that loading state is shown
const extractedData = data?.map((item) => item.node);
return (
<div className="h-full relative">
<LedgerTable
ref={gridRef}
rowData={extractedData}
rowData={data}
onFilterChanged={onFilterChanged}
overlayNoRowsTemplate={error ? error.message : t('No entries')}
/>
{extractedData && (
<LedgerExportLink entries={extractedData} partyId={partyId} />
)}
{data && <LedgerExportLink entries={data} partyId={partyId} />}
</div>
);
};
+1 -1
View File
@@ -55,7 +55,7 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }) => data.id}
getRowId={({ data }) => `${data.party.id}-${data.status}`}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
-6
View File
@@ -1,6 +0,0 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
@@ -1,29 +1,14 @@
import { forwardRef } from 'react';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type {
VegaValueGetterParams,
VegaValueFormatterParams,
VegaICellRendererParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
PriceFlashCell,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketTableActions } from './market-table-actions';
import { OracleStatus } from './oracle-status';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
const { MarketTradingMode, AuctionTrigger } = Schema;
import { useColumnDefs } from './use-column-defs';
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
@@ -51,199 +36,34 @@ const MarketName = (props: MarketNameCellProps) => (
</>
);
const defaultColDef = {
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
};
export const MarketListTable = forwardRef<
AgGridReact,
TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
}
>(({ onMarketClick, ...props }, ref) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const columnDefs = useColumnDefs({ onMarketClick });
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
getRowId={getRowId}
ref={ref}
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
}}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
suppressCellFocus
components={{ PriceFlashCell, MarketName }}
storeKey="allMarkets"
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="tradableInstrument.instrument.code"
cellRenderer="MarketName"
cellRendererParams={{ onMarketClick }}
/>
<AgGridColumn
headerName={t('Description')}
field="tradableInstrument.instrument.name"
/>
<AgGridColumn
headerName={t('Trading mode')}
field="tradingMode"
minWidth={170}
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return undefined;
const { trigger, marketTradingMode } = data.data;
return marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[marketTradingMode];
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketTradingModeMapping,
}}
/>
<AgGridColumn
headerName={t('Status')}
field="state"
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketStateMapping,
}}
/>
<AgGridColumn
headerName={t('Best bid')}
field="data.bestBidPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) =>
data?.data?.bestBidPrice === undefined
? undefined
: addDecimalsFormatNumber(
data.data.bestBidPrice,
data.decimalPlaces
)
}
/>
<AgGridColumn
headerName={t('Best offer')}
field="data.bestOfferPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) =>
data?.data?.bestOfferPrice === undefined
? undefined
: addDecimalsFormatNumber(
data.data.bestOfferPrice,
data.decimalPlaces
)
}
/>
<AgGridColumn
headerName={t('Mark price')}
field="data.markPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined
? undefined
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces)
}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset.symbol"
cellRenderer={({
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
}}
/>
<AgGridColumn
colId="market-actions"
field="id"
{...COL_DEFS.actions}
cellRenderer={({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketTableActions
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
}}
/>
</AgGrid>
/>
);
});
@@ -0,0 +1,107 @@
import { render, screen, act, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as DataProviders from '@vegaprotocol/data-provider';
import { MockedProvider } from '@apollo/react-testing';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketsContainer } from './markets-container';
const market = {
id: 'id-1',
tradableInstrument: {
instrument: {
product: { settlementAsset: { id: 'assetId-1' } },
},
},
decimalPlaces: 1,
positionDecimalPlaces: 1,
state: 'STATE_ACTIVE',
tradingMode: 'TRADING_MODE_OPENING_AUCTION',
data: {
bestBidPrice: 100,
},
} as unknown as MarketMaybeWithData;
describe('MarketsContainer', () => {
it('context menu should stay open', async () => {
const spyOnSelect = jest.fn();
jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
.mockImplementation(() => {
return {
error: null,
reload: jest.fn(),
data: [market],
};
});
let rerenderRef: (ui: React.ReactElement) => void;
await act(async () => {
const { rerender } = render(
<MockedProvider>
<MarketsContainer onSelect={spyOnSelect} />
</MockedProvider>
);
rerenderRef = rerender;
});
// make sure ag grid is finished initializaing
const rowContainer = await screen.findByRole('rowgroup', {
name: (_name, element) =>
element.classList.contains('ag-center-cols-container'),
});
expect(within(rowContainer).getAllByRole('row')).toHaveLength(1);
expect(
screen.getByRole('rowgroup', {
name: (_name, element) =>
element.classList.contains('ag-pinned-right-cols-container'),
})
).toBeInTheDocument();
// open the dropdown
await userEvent.click(
screen.getByRole('button', {
name: (_name, element) =>
(element.parentNode as Element)?.getAttribute('id') ===
'cell-market-actions-8',
})
);
await checkDropdown();
// reset the mock and rerender so the component
// updates with new data
jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
.mockImplementation(() => {
return {
error: null,
reload: jest.fn(),
data: [{ ...market, state: 'STATE_PENDING' }],
};
});
// @ts-ignore we await the act above so rerenderRef is definitely defined
rerenderRef(
<MockedProvider mocks={[]}>
<MarketsContainer onSelect={spyOnSelect} />
</MockedProvider>
);
// make sure dropdown is still open
await checkDropdown();
async function checkDropdown() {
const dropdownContent = await screen.findByTestId(
'market-actions-content'
);
expect(dropdownContent).toBeInTheDocument();
expect(
within(dropdownContent).getByRole('menuitem', {
name: 'Copy Market ID',
})
).toBeInTheDocument();
}
});
});
@@ -0,0 +1,196 @@
import { useMemo } from 'react';
import type { ColDef } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketTableActions } from './market-table-actions';
interface Props {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
}
const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = ({ onMarketClick }: Props) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>(
() => [
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
cellRenderer: 'MarketName',
cellRendererParams: { onMarketClick },
},
{
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Trading mode'),
field: 'tradingMode',
minWidth: 170,
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return '-';
const { trigger, marketTradingMode } = data.data;
return marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[marketTradingMode];
},
filter: SetFilter,
filterParams: {
set: Schema.MarketTradingModeMapping,
},
},
{
headerName: t('Status'),
field: 'state',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
},
filter: SetFilter,
filterParams: {
set: Schema.MarketStateMapping,
},
},
{
headerName: t('Best bid'),
field: 'data.bestBidPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) =>
data?.data?.bestBidPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestBidPrice,
data.decimalPlaces
),
},
{
headerName: t('Best offer'),
field: 'data.bestOfferPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestOfferPrice,
data.decimalPlaces
),
},
{
headerName: t('Mark price'),
field: 'data.markPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces),
},
{
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
cellRenderer: ({
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
},
},
{
colId: 'market-actions',
field: 'id',
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketTableActions
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
},
},
],
[onMarketClick, openAssetDetailsDialog]
);
};
@@ -1,23 +1,23 @@
import { update } from './order-data-provider';
import {
update,
mapOrderUpdateToOrder,
filterOrderUpdates,
} from './order-data-provider';
import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../';
import type { Edge } from '@vegaprotocol/data-provider';
describe('order data provider', () => {
it('puts incoming data in proper place', () => {
const data = [
{
node: {
id: '2',
createdAt: new Date('2022-01-29').toISOString(),
},
id: '2',
createdAt: new Date('2022-01-29').toISOString(),
},
{
node: {
id: '1',
createdAt: new Date('2022-01-28').toISOString(),
},
id: '1',
createdAt: new Date('2022-01-28').toISOString(),
},
] as Edge<OrderFieldsFragment>[];
] as OrderFieldsFragment[];
const delta = [
// this one should be dropped because id don't exits and it's older than newest
@@ -52,39 +52,41 @@ describe('order data provider', () => {
createdAt: new Date('2022-02-05').toISOString(),
},
] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, { partyId: '0x123' });
const updatedData = update(
data,
filterOrderUpdates(delta),
{ partyId: '0x123' },
mapOrderUpdateToOrder
);
expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual(
-1
);
expect(updatedData && updatedData[3].id).toEqual(delta[2].id);
expect(updatedData && updatedData[3].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[0].id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[4].updatedAt);
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
expect(updatedData && updatedData[3].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[3].node.updatedAt).toEqual(
delta[2].updatedAt
);
expect(updatedData && updatedData[0].node.id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].node.id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[4].updatedAt
);
expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual(
5
);
update(
[],
filterOrderUpdates(delta),
{ partyId: '0x123' },
mapOrderUpdateToOrder
)?.length
).toEqual(5);
});
it('add only data matching date range filter', () => {
const data = [
{
node: {
id: '1',
createdAt: new Date('2022-01-29').toISOString(),
},
id: '1',
createdAt: new Date('2022-01-29').toISOString(),
},
{
node: {
id: '2',
createdAt: new Date('2022-01-30').toISOString(),
},
id: '2',
createdAt: new Date('2022-01-30').toISOString(),
},
] as Edge<OrderFieldsFragment>[];
] as OrderFieldsFragment[];
const delta = [
// this one should be ignored because it does not match date range
@@ -105,22 +107,23 @@ describe('order data provider', () => {
},
] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, {
partyId: '0x123',
filter: {
dateRange: { end: new Date('2022-02-01').toISOString() },
const updatedData = update(
data,
filterOrderUpdates(delta),
{
partyId: '0x123',
filter: {
dateRange: { end: new Date('2022-02-01').toISOString() },
},
},
});
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].node.updatedAt).toEqual(
delta[2].updatedAt
mapOrderUpdateToOrder
);
expect(updatedData && updatedData[2].node.id).toEqual(delta[1].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[1].updatedAt
expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual(
-1
);
expect(updatedData && updatedData[0].id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[2].id).toEqual(delta[1].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[1].updatedAt);
});
});
@@ -6,8 +6,8 @@ import {
defaultAppend as append,
} from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import { marketsMapProvider } from '@vegaprotocol/markets';
import type { PageInfo, Edge, Cursor } from '@vegaprotocol/data-provider';
import { OrderStatus } from '@vegaprotocol/types';
import type {
OrderFieldsFragment,
@@ -15,6 +15,7 @@ import type {
OrdersQuery,
OrdersUpdateSubscription,
OrdersQueryVariables,
OrdersUpdateSubscriptionVariables,
} from './__generated__/Orders';
import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
import type { ApolloClient } from '@apollo/client';
@@ -37,18 +38,21 @@ const orderMatchFilters = (
if (!order) {
return true;
}
if (
variables?.filter?.status &&
!(order.status && variables.filter.status.includes(order.status))
) {
return false;
}
if (
variables?.filter?.liveOnly &&
!(order.status && liveOnlyOrderStatuses.includes(order.status))
) {
return false;
}
if (
variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type))
@@ -76,10 +80,11 @@ const orderMatchFilters = (
) {
return false;
}
return true;
};
const mapOrderUpdateToOrder = (
export const mapOrderUpdateToOrder = (
orderUpdate: OrderUpdateFieldsFragment
): OrderFieldsFragment => {
const { marketId, liquidityProvisionId, ...order } = orderUpdate;
@@ -101,10 +106,36 @@ const mapOrderUpdateToOrder = (
};
};
const mapOrderUpdateToOrderWithMarket =
(markets: Record<string, Market>) =>
(orderUpdate: OrderUpdateFieldsFragment): Order => {
const { market, ...order } = mapOrderUpdateToOrder(orderUpdate);
return {
...order,
market: markets[market.id],
};
};
const getData = (
responseData: OrdersQuery | null
): Edge<OrderFieldsFragment>[] =>
responseData?.party?.ordersConnection?.edges || [];
): (OrderFieldsFragment & Cursor)[] =>
responseData?.party?.ordersConnection?.edges?.map<
OrderFieldsFragment & Cursor
>((edge) => ({ ...edge.node, cursor: edge.cursor })) || [];
export const filterOrderUpdates = (
orders: OrdersUpdateSubscription['orders']
) => {
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
return orderBy(
uniqBy(
orderBy(orders, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
};
const getDelta = (
subscriptionData: OrdersUpdateSubscription,
@@ -114,49 +145,32 @@ const getDelta = (
if (!subscriptionData.orders) {
return [];
}
return subscriptionData.orders;
return filterOrderUpdates(subscriptionData.orders);
};
export const update = (
data: ReturnType<typeof getData> | null,
export const update = <T extends Omit<OrderFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
reload: () => void,
variables?: OrdersQueryVariables
) => {
if (!data) {
return data;
}
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
const incoming = orderBy(
uniqBy(
orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
const updatedData = [...data];
incoming.forEach((orderUpdate) => {
const index = data.findIndex((edge) => edge.node.id === orderUpdate.id);
const newer =
data.length === 0 || orderUpdate.createdAt >= data[0].node.createdAt;
variables: OrdersQueryVariables,
mapDeltaToData: (delta: OrderUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
delta.forEach((orderUpdate) => {
const index = data?.findIndex((order) => order.id === orderUpdate.id) ?? -1;
const newer = !data?.length || orderUpdate.createdAt >= data[0].createdAt;
const doesFilterPass =
!variables || orderMatchFilters(orderUpdate, variables);
if (index !== -1) {
if (doesFilterPass) {
updatedData[index] = {
...updatedData[index],
node: mapOrderUpdateToOrder(orderUpdate),
...mapDeltaToData(orderUpdate),
};
} else {
updatedData.splice(index, 1);
}
} else if (newer && doesFilterPass) {
updatedData.unshift({
node: mapOrderUpdateToOrder(orderUpdate),
cursor: '',
});
updatedData.unshift(mapDeltaToData(orderUpdate));
}
});
return updatedData;
@@ -170,11 +184,13 @@ export const ordersProvider = makeDataProvider<
ReturnType<typeof getData>,
OrdersUpdateSubscription,
ReturnType<typeof getDelta>,
OrdersQueryVariables
OrdersQueryVariables,
OrdersUpdateSubscriptionVariables
>({
query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument,
update,
update: (data, delta, reload, variables) =>
update(data, delta, variables, mapOrderUpdateToOrder),
getData,
getDelta,
pagination: {
@@ -185,6 +201,10 @@ export const ordersProvider = makeDataProvider<
resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true },
fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ partyId, marketIds }) => ({
partyId,
marketIds,
}),
});
export const activeOrdersProvider = makeDerivedDataProvider<
@@ -208,27 +228,36 @@ export const activeOrdersProvider = makeDerivedDataProvider<
}
const orders = partsData[0] as ReturnType<typeof getData>;
return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id)
? orders.filter((order) => variables.marketId === order.market.id)
: orders;
}
);
export const ordersWithMarketProvider = makeDerivedDataProvider<
(Order | null)[],
Order[],
(Order & Cursor)[],
never,
OrdersQueryVariables
>(
[
ordersProvider,
(callback, client) => marketsProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(partsData): Order[] =>
((partsData[0] as ReturnType<typeof getData>) || []).map((edge) => ({
...edge.node,
market: (partsData[1] as Market[]).find(
(market) => market.id === edge.node.market.id
),
}))
(partsData, variables, prevData, parts): Order[] => {
if (prevData && parts[0].isUpdate) {
return update(
prevData,
parts[0].delta,
variables,
mapOrderUpdateToOrderWithMarket(partsData[1] as Record<string, Market>)
);
}
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
(order) => ({
...order,
market: (partsData[1] as Record<string, Market>)[order.market.id],
})
);
}
);
export const hasActiveOrderProvider = makeDerivedDataProvider<
@@ -244,7 +273,7 @@ export const hasAmendableOrderProvider = makeDerivedDataProvider<
>([activeOrdersProvider], (parts) => {
const activeOrders = parts[0] as ReturnType<typeof getData>;
const hasAmendableOrder = activeOrders.some(
(edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder)
(order) => !(order.liquidityProvision || order.peggedOrder)
);
return hasAmendableOrder;
});
@@ -32,7 +32,6 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
totalCount: undefined,
});
await act(async () => {
render(generateJsx());
@@ -87,7 +87,7 @@ export const OrderListManager = ({
gridRef.current.api.setRowData(data);
return true;
}
return true;
return false;
},
});
+20 -2
View File
@@ -47,7 +47,7 @@ const positionFields: PositionFieldsFragment[] = [
{
__typename: 'Position',
realisedPNL: '230000',
openVolume: '6',
openVolume: '-6',
unrealisedPNL: '895000',
averageEntryPrice: '1129935',
updatedAt: '2022-07-28T15:09:34.441143Z',
@@ -82,7 +82,7 @@ const positionFields: PositionFieldsFragment[] = [
},
{
__typename: 'Position',
realisedPNL: '230000',
realisedPNL: '-230000',
openVolume: '1',
unrealisedPNL: '-22519',
averageEntryPrice: '84400088',
@@ -98,6 +98,24 @@ const positionFields: PositionFieldsFragment[] = [
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
{
__typename: 'Position',
realisedPNL: '-303295252',
openVolume: '0',
unrealisedPNL: '0',
averageEntryPrice: '6126312',
updatedAt: '2022-07-28T14:53:54.725477Z',
positionStatus: PositionStatus.POSITION_STATUS_CLOSED_OUT,
lossSocializationAmount: '261',
market: {
id: 'market-3',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
},
];
const marginsFields: MarginFieldsFragment[] = [
+22 -21
View File
@@ -10,32 +10,33 @@ fragment TradeFields on Trade {
}
query Trades($marketId: ID!, $pagination: Pagination) {
market(id: $marketId) {
id
tradesConnection(pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
trades(filter: { marketIds: [$marketId] }, pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
subscription TradesUpdate($marketId: ID!) {
trades(marketId: $marketId) {
id
price
size
createdAt
marketId
aggressor
tradesStream(filter: { marketIds: [$marketId] }) {
...TradeUpdateFields
}
}
+28 -24
View File
@@ -11,14 +11,16 @@ export type TradesQueryVariables = Types.Exact<{
}>;
export type TradesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradesConnection?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null } | null };
export type TradesQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type TradeUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side };
export type TradesUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type TradesUpdateSubscription = { __typename?: 'Subscription', trades?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null };
export type TradesUpdateSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null };
export const TradeFieldsFragmentDoc = gql`
fragment TradeFields on Trade {
@@ -32,23 +34,30 @@ export const TradeFieldsFragmentDoc = gql`
}
}
`;
export const TradeUpdateFieldsFragmentDoc = gql`
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
`;
export const TradesDocument = gql`
query Trades($marketId: ID!, $pagination: Pagination) {
market(id: $marketId) {
id
tradesConnection(pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
trades(filter: {marketIds: [$marketId]}, pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
@@ -84,16 +93,11 @@ export type TradesLazyQueryHookResult = ReturnType<typeof useTradesLazyQuery>;
export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>;
export const TradesUpdateDocument = gql`
subscription TradesUpdate($marketId: ID!) {
trades(marketId: $marketId) {
id
price
size
createdAt
marketId
aggressor
tradesStream(filter: {marketIds: [$marketId]}) {
...TradeUpdateFields
}
}
`;
${TradeUpdateFieldsFragmentDoc}`;
/**
* __useTradesUpdateSubscription__
+1 -3
View File
@@ -1,4 +1,3 @@
import compact from 'lodash/compact';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react';
@@ -19,12 +18,11 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
dataProvider: tradesWithMarketProvider,
variables: { marketId },
});
const trades = compact(data).map((d) => d.node);
return (
<TradesTable
ref={gridRef}
rowData={trades}
rowData={data}
onClick={(price?: string) => {
if (price) {
updateOrder(marketId, { price });
+75 -57
View File
@@ -3,75 +3,95 @@ import {
makeDerivedDataProvider,
defaultAppend as append,
} from '@vegaprotocol/data-provider';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import type { PageInfo, Cursor } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets';
import type {
TradesQuery,
TradesQueryVariables,
TradeFieldsFragment,
TradesUpdateSubscription,
TradeUpdateFieldsFragment,
TradesUpdateSubscriptionVariables,
} from './__generated__/Trades';
import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades';
import orderBy from 'lodash/orderBy';
import produce from 'immer';
export const MAX_TRADES = 500;
const getData = (
responseData: TradesQuery | null
): ({
cursor: string;
node: TradeFieldsFragment;
} | null)[] => responseData?.market?.tradesConnection?.edges || [];
): (TradeFieldsFragment & Cursor)[] =>
responseData?.trades?.edges.map<TradeFieldsFragment & Cursor>((edge) => ({
...edge.node,
cursor: edge.cursor,
})) || [];
const getDelta = (subscriptionData: TradesUpdateSubscription) =>
subscriptionData?.trades || [];
subscriptionData?.tradesStream || [];
const update = (
data: ReturnType<typeof getData> | null,
delta: ReturnType<typeof getDelta>
) => {
if (!data) return data;
return produce(data, (draft) => {
// for each incoming trade add it to the beginning and remove oldest trade
orderBy(delta, 'createdAt', 'desc').forEach((node) => {
const { marketId, ...nodeData } = node;
draft.unshift({
node: {
...nodeData,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
},
cursor: '',
});
const mapTradeUpdateToTrade = (
tradeUpdate: TradeUpdateFieldsFragment
): TradeFieldsFragment => {
const { marketId, ...trade } = tradeUpdate;
return {
...trade,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
};
};
if (draft.length > MAX_TRADES) {
draft.pop();
}
});
const mapTradeUpdateToTradeWithMarket =
(markets: Record<string, Market>) =>
(tradeUpdate: TradeUpdateFieldsFragment): Trade => {
const { market, ...trade } = mapTradeUpdateToTrade(tradeUpdate);
return {
...trade,
market: markets[market.id],
};
};
const update = <T extends Omit<TradeFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
variables: TradesQueryVariables,
mapDeltaToData: (delta: TradeUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
orderBy(delta, 'createdAt', 'desc').forEach((tradeUpdate) => {
const index = data?.findIndex((trade) => trade.id === tradeUpdate.id) ?? -1;
if (index !== -1) {
updatedData[index] = {
...updatedData[index],
...mapDeltaToData(tradeUpdate),
};
} else if (!data?.length || tradeUpdate.createdAt >= data[0].createdAt) {
updatedData.unshift(mapDeltaToData(tradeUpdate));
}
});
return updatedData.slice(0, MAX_TRADES);
};
export type Trade = Omit<TradeFieldsFragment, 'market'> & { market?: Market };
export type TradeEdge = Edge<Trade>;
const getPageInfo = (responseData: TradesQuery | null): PageInfo | null =>
responseData?.market?.tradesConnection?.pageInfo || null;
responseData?.trades?.pageInfo || null;
export const tradesProvider = makeDataProvider<
Parameters<typeof getData>['0'],
ReturnType<typeof getData>,
Parameters<typeof getDelta>['0'],
ReturnType<typeof getDelta>,
TradesQueryVariables
TradesQueryVariables,
TradesUpdateSubscriptionVariables
>({
query: TradesDocument,
subscriptionQuery: TradesUpdateDocument,
update,
update: (data, delta, reload, variables) =>
update(data, delta, variables, mapTradeUpdateToTrade),
getData,
getDelta,
pagination: {
@@ -79,34 +99,32 @@ export const tradesProvider = makeDataProvider<
append,
first: MAX_TRADES,
},
getSubscriptionVariables: ({ marketId }) => ({ marketId }),
});
export const tradesWithMarketProvider = makeDerivedDataProvider<
(TradeEdge | null)[],
Trade[],
(Trade & Cursor)[],
never,
TradesQueryVariables
>(
[
tradesProvider,
(callback, client) => marketsProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(partsData): (TradeEdge | null)[] | null => {
const edges = partsData[0] as ReturnType<typeof getData>;
return edges.map((edge) => {
if (edge === null) {
return null;
}
const node = {
...edge.node,
market: (partsData[1] as Market[]).find(
(market) => market.id === edge.node.market.id
),
};
const cursor = edge?.cursor || '';
return {
cursor,
node,
};
});
(partsData, variables, prevData, parts): Trade[] | null => {
if (prevData && parts[0].isUpdate) {
return update(
prevData,
parts[0].delta as ReturnType<typeof getDelta>,
variables,
mapTradeUpdateToTradeWithMarket(partsData[1] as Record<string, Market>)
);
}
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
(trade) => ({
...trade,
market: (partsData[1] as Record<string, Market>)[trade.market.id],
})
);
}
);
+14 -18
View File
@@ -11,24 +11,20 @@ export const tradesQuery = (
override?: PartialDeep<TradesQuery>
): TradesQuery => {
const defaultResult: TradesQuery = {
market: {
id: 'market-0',
tradesConnection: {
__typename: 'TradeConnection',
edges: trades.map((node, i) => ({
__typename: 'TradeEdge',
node,
cursor: (i + 1).toString(),
})),
pageInfo: {
__typename: 'PageInfo',
startCursor: '0',
endCursor: trades.length.toString(),
hasNextPage: false,
hasPreviousPage: false,
},
trades: {
__typename: 'TradeConnection',
edges: trades.map((node, i) => ({
__typename: 'TradeEdge',
node,
cursor: (i + 1).toString(),
})),
pageInfo: {
__typename: 'PageInfo',
startCursor: '0',
endCursor: trades.length.toString(),
hasNextPage: false,
hasPreviousPage: false,
},
__typename: 'Market',
},
};
@@ -40,7 +36,7 @@ export const tradesUpdateSubscription = (
): TradesUpdateSubscription => {
const defaultResult: TradesUpdateSubscription = {
__typename: 'Subscription',
trades: [
tradesStream: [
{
__typename: 'TradeUpdate',
id: '1234567890',
@@ -1,4 +1,4 @@
@for $i from 0 through 16 {
@for $i from 0 through 25 {
.loader-item:nth-child(#{$i}) {
@if $i % 2 == 0 {
animation-delay: #{$i * 50 * random(5)}ms;
@@ -25,7 +25,7 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
});
const wrapperClasses =
size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]';
const items = size === 'small' ? 9 : 16;
const items = size === 'small' ? 9 : 25;
const generate = useMemo(() => pseudoRandom(1), []);
+1
View File
@@ -13,3 +13,4 @@ export * from './lib/remove-0x';
export * from './lib/remove-pagination-wrapper';
export * from './lib/time';
export * from './lib/validate';
export * from './lib/resolve-network-name';
@@ -0,0 +1,6 @@
const NETWORK_NAME_MAP: Readonly<Record<string, string>> = {
'1': 'Ethereum Mainnet',
'11155111': 'Sepolia test network',
};
export const resolveNetworkName = (chainId?: string): string =>
NETWORK_NAME_MAP[chainId || ''] || `(chainID: ${chainId})`;
@@ -9,7 +9,10 @@ import { Intent } from '@vegaprotocol/ui-toolkit';
import { useCallback } from 'react';
import compact from 'lodash/compact';
import type { EthWithdrawalApprovalState } from './use-ethereum-withdraw-approvals-store';
import { useEthWithdrawApprovalsStore } from './use-ethereum-withdraw-approvals-store';
import {
useEthWithdrawApprovalsStore,
WithdrawalFailure,
} from './use-ethereum-withdraw-approvals-store';
import { ApprovalStatus } from './use-ethereum-withdraw-approvals-store';
import { VerificationStatus } from './withdrawal-approval-status';
@@ -26,12 +29,23 @@ const EthWithdrawalApprovalToastContent = ({
}: {
tx: EthWithdrawalApprovalState;
}) => {
const isConnectionFailure =
tx.failureReason &&
[
WithdrawalFailure.WrongConnection,
WithdrawalFailure.NoConnection,
].includes(tx.failureReason);
let title = '';
if (tx.status === ApprovalStatus.Error) {
title = t('Error occurred');
}
if (tx.status === ApprovalStatus.Pending) {
title = t('Pending approval');
if (tx.failureReason && isConnectionFailure) {
title = tx.message || t('Withdraw failure');
} else {
title = t('Pending approval');
}
}
if (tx.status === ApprovalStatus.Delayed) {
title = t('Delayed');
@@ -39,11 +53,12 @@ const EthWithdrawalApprovalToastContent = ({
if (tx.status === ApprovalStatus.Ready) {
title = t('Approved');
}
const num = formatNumber(
toBigNum(tx.withdrawal.amount, tx.withdrawal.asset.decimals),
tx.withdrawal.asset.decimals
);
const details = (
const details = isConnectionFailure ? null : (
<Panel>
<strong>
{t('Withdraw')} {num} {tx.withdrawal.asset.symbol}
@@ -61,7 +76,8 @@ const EthWithdrawalApprovalToastContent = ({
};
const isFinal = (tx: EthWithdrawalApprovalState) =>
[ApprovalStatus.Ready, ApprovalStatus.Error].includes(tx.status);
[ApprovalStatus.Ready, ApprovalStatus.Error].includes(tx.status) &&
!tx.failureReason;
export const useEthereumWithdrawApprovalsToasts = () => {
const [setToast, remove] = useToasts((state) => [
@@ -74,21 +90,34 @@ export const useEthereumWithdrawApprovalsToasts = () => {
]);
const fromWithdrawalApproval = useCallback(
(tx: EthWithdrawalApprovalState): Toast => ({
id: `withdrawal-${tx.id}`,
intent: intentMap[tx.status],
onClose: () => {
if ([ApprovalStatus.Error, ApprovalStatus.Ready].includes(tx.status)) {
deleteTx(tx.id);
} else {
dismissTx(tx.id);
}
remove(`withdrawal-${tx.id}`);
},
loader: tx.status === ApprovalStatus.Pending,
content: <EthWithdrawalApprovalToastContent tx={tx} />,
closeAfter: isFinal(tx) ? CLOSE_AFTER : undefined,
}),
(tx: EthWithdrawalApprovalState): Toast => {
const loader =
tx.status === ApprovalStatus.Pending &&
!(
tx.failureReason &&
[
WithdrawalFailure.WrongConnection,
WithdrawalFailure.NoConnection,
].includes(tx.failureReason)
);
return {
id: `withdrawal-${tx.id}`,
intent: intentMap[tx.status],
onClose: () => {
if (
[ApprovalStatus.Error, ApprovalStatus.Ready].includes(tx.status)
) {
deleteTx(tx.id);
} else {
dismissTx(tx.id);
}
remove(`withdrawal-${tx.id}`);
},
loader,
content: <EthWithdrawalApprovalToastContent tx={tx} />,
closeAfter: isFinal(tx) ? CLOSE_AFTER : undefined,
};
},
[deleteTx, dismissTx, remove]
);
@@ -5,7 +5,10 @@ import type { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import waitForNextTick from 'flush-promises';
import * as Schema from '@vegaprotocol/types';
import { ApprovalStatus } from './use-ethereum-withdraw-approvals-store';
import {
ApprovalStatus,
WithdrawalFailure,
} from './use-ethereum-withdraw-approvals-store';
import BigNumber from 'bignumber.js';
import type {
EthWithdrawApprovalStore,
@@ -21,7 +24,7 @@ import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
const mockWeb3Provider = jest.fn();
let mockChainId = 111111;
let mockChainId: number | undefined = 111111;
jest.mock('@web3-react/core', () => ({
useWeb3React: () => ({
provider: mockWeb3Provider(),
@@ -314,9 +317,29 @@ describe('useEthWithdrawApprovalsManager', () => {
update,
});
render();
expect(update.mock.calls[0][1].status).toEqual(ApprovalStatus.Error);
expect(update.mock.calls[0][1].status).toEqual(ApprovalStatus.Pending);
expect(update.mock.calls[0][1].message).toEqual('Change network');
expect(update.mock.calls[0][1].failureReason).toEqual(
WithdrawalFailure.WrongConnection
);
mockChainId = 111111;
});
it('detect no chainId', () => {
mockChainId = undefined;
const transaction = createWithdrawTransaction();
mockEthTransactionStoreState.mockReturnValue({ create });
mockEthWithdrawApprovalsStoreState.mockReturnValue({
transactions: [transaction],
update,
});
render();
expect(update.mock.calls[0][1].status).toEqual(ApprovalStatus.Pending);
expect(update.mock.calls[0][1].message).toEqual(
'You are on the wrong network'
'Connect wallet to withdraw'
);
expect(update.mock.calls[0][1].failureReason).toEqual(
WithdrawalFailure.NoConnection
);
mockChainId = 111111;
});
@@ -1,6 +1,6 @@
import { useApolloClient } from '@apollo/client';
import BigNumber from 'bignumber.js';
import { useRef, useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { addDecimal } from '@vegaprotocol/utils';
import { useGetWithdrawThreshold } from './use-get-withdraw-threshold';
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
@@ -21,8 +21,9 @@ import { WithdrawalApprovalDocument } from '@vegaprotocol/wallet';
import { useEthTransactionStore } from './use-ethereum-transaction-store';
import {
useEthWithdrawApprovalsStore,
ApprovalStatus,
useEthWithdrawApprovalsStore,
WithdrawalFailure,
} from './use-ethereum-withdraw-approvals-store';
export const useEthWithdrawApprovalsManager = () => {
@@ -55,16 +56,27 @@ export const useEthWithdrawApprovalsManager = () => {
message: t(
`Invalid asset source: ${withdrawal.asset.source.__typename}`
),
failureReason: WithdrawalFailure.InvalidAsset,
});
return;
}
if (!chainId) {
update(transaction.id, {
status: ApprovalStatus.Pending,
message: t(`Connect wallet to withdraw`),
failureReason: WithdrawalFailure.NoConnection,
});
return;
}
if (chainId?.toString() !== config?.chain_id) {
update(transaction.id, {
status: ApprovalStatus.Error,
message: t(`You are on the wrong network`),
status: ApprovalStatus.Pending,
message: t(`Change network`),
failureReason: WithdrawalFailure.WrongConnection,
});
return;
}
update(transaction.id, {
status: ApprovalStatus.Pending,
message: t('Verifying withdrawal approval'),
@@ -14,6 +14,13 @@ export enum ApprovalStatus {
Error = 'Error',
Ready = 'Ready',
}
export enum WithdrawalFailure {
InvalidAsset,
NoConnection,
WrongConnection,
}
export interface EthWithdrawalApprovalState {
id: number;
createdAt: Date;
@@ -24,6 +31,7 @@ export interface EthWithdrawalApprovalState {
dialogOpen?: boolean;
withdrawal: WithdrawalBusEventFieldsFragment;
approval?: WithdrawalApprovalQuery['erc20WithdrawalApproval'];
failureReason?: WithdrawalFailure;
}
export interface EthWithdrawApprovalStore {
transactions: (EthWithdrawalApprovalState | undefined)[];
@@ -42,6 +50,7 @@ export interface EthWithdrawApprovalStore {
| 'threshold'
| 'completeTimestamp'
| 'dialogOpen'
| 'failureReason'
>
>
) => void;
@@ -86,6 +95,7 @@ export const useEthWithdrawApprovalsStore = create<EthWithdrawApprovalStore>()(
| 'threshold'
| 'completeTimestamp'
| 'dialogOpen'
| 'failureReason'
>
>
) =>
@@ -1,17 +1,69 @@
import { t } from '@vegaprotocol/i18n';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import {
getDateTimeFormat,
resolveNetworkName,
truncateByChars,
} from '@vegaprotocol/utils';
import type { EthWithdrawalApprovalState } from './use-ethereum-withdraw-approvals-store';
import { ApprovalStatus } from './use-ethereum-withdraw-approvals-store';
import {
ApprovalStatus,
useEthWithdrawApprovalsStore,
WithdrawalFailure,
} from './use-ethereum-withdraw-approvals-store';
import { useEthereumConfig } from './use-ethereum-config';
import { Button, useToasts } from '@vegaprotocol/ui-toolkit';
import { useWeb3ConnectStore } from './web3-connect-store';
export const VerificationStatus = ({
state,
}: {
state: EthWithdrawalApprovalState;
}) => {
const { config } = useEthereumConfig();
const openDialog = useWeb3ConnectStore((state) => state.open);
const remove = useToasts((state) => state.remove);
const deleteTx = useEthWithdrawApprovalsStore((state) => state.delete);
if (state.status === ApprovalStatus.Error) {
return <p>{state.message || t('Something went wrong')}</p>;
}
if (
state.failureReason &&
[
WithdrawalFailure.WrongConnection,
WithdrawalFailure.NoConnection,
].includes(state.failureReason)
) {
return state.failureReason === WithdrawalFailure.NoConnection ? (
<>
<p>
{t('To complete this withdrawal, connect the Ethereum wallet %s', [
truncateByChars(state.withdrawal.details?.receiverAddress || ' '),
])}
</p>
<Button
onClick={() => {
openDialog();
deleteTx(state.id);
remove(`withdrawal-${state.id}`);
}}
>
{t('Connect wallet')}
</Button>
</>
) : (
<>
<p>{t('Your Ethereum wallet is connected to the wrong network.')}</p>
<p className="mt-2">
{t('Go to your Ethereum wallet and connect to the network %s', [
resolveNetworkName(config?.chain_id),
])}
</p>
</>
);
}
if (state.status === ApprovalStatus.Pending) {
return <p>{t('Verifying...')}</p>;
}
+1 -1
View File
@@ -161,7 +161,7 @@
"babel-loader": "8.1.0",
"cypress": "^12.9.0",
"cypress-mochawesome-reporter": "^3.3.0",
"cypress-real-events": "^1.7.6",
"cypress-real-events": "^1.8.1",
"dotenv": "^16.0.1",
"eslint": "8.15.0",
"eslint-config-next": "12.2.3",
+4 -4
View File
@@ -11882,10 +11882,10 @@ cypress-mochawesome-reporter@^3.3.0:
mochawesome-merge "^4.2.1"
mochawesome-report-generator "^6.2.0"
cypress-real-events@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.6.tgz#6f17e0b2ceea1d6dc60f6737d8f84cc517bbbb4c"
integrity sha512-yP6GnRrbm6HK5q4DH6Nnupz37nOfZu/xn1xFYqsE2o4G73giPWQOdu6375QYpwfU1cvHNCgyD2bQ2hPH9D7NMw==
cypress-real-events@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.8.1.tgz#d00c7fe93124bbe7c0f27296684838614d24a840"
integrity sha512-8fFnA8EzS3EVbAmpSEUf3A8yZCmfU3IPOSGUDVFCdE1ke1gYL1A+gvXXV6HKUbTPRuvKKt2vpaMbUwYLpDRswQ==
cypress@^12.9.0:
version "12.9.0"