Merge branch 'master' of github.com:vegaprotocol/frontend-monorepo

This commit is contained in:
madalinaraicu
2022-07-21 13:06:52 +03:00
94 changed files with 4465 additions and 1212 deletions
@@ -52,7 +52,7 @@ jobs:
uses: actions/checkout@v2
with:
repository: vegaprotocol/vegacapsule
ref: main
ref: v0.2.1
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './capsule'
@@ -72,6 +72,18 @@ jobs:
GITHUB_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
GOBIN: ${{ env.GOBIN }}
- name: Checkout Vega
uses: actions/checkout@v2
with:
repository: vegaprotocol/vega
ref: develop
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './vega'
- name: Install binary from Vega repo
run: go install ./cmd/vega
working-directory: vega
######
## Start capsule
######
+13 -1
View File
@@ -36,7 +36,7 @@ jobs:
uses: actions/checkout@v2
with:
repository: vegaprotocol/vegacapsule
ref: main
ref: v0.2.1
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './capsule'
@@ -56,6 +56,18 @@ jobs:
GITHUB_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
GOBIN: ${{ env.GOBIN }}
- name: Checkout Vega
uses: actions/checkout@v2
with:
repository: vegaprotocol/vega
ref: develop
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './vega'
- name: Install binary from Vega repo
run: go install ./cmd/vega
working-directory: vega
######
## Start capsule
######
+13 -1
View File
@@ -38,7 +38,7 @@ jobs:
uses: actions/checkout@v2
with:
repository: vegaprotocol/vegacapsule
ref: main
ref: v0.2.1
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './capsule'
@@ -58,6 +58,18 @@ jobs:
GITHUB_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
GOBIN: ${{ env.GOBIN }}
- name: Checkout Vega
uses: actions/checkout@v2
with:
repository: vegaprotocol/vega
ref: develop
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './vega'
- name: Install binary from Vega repo
run: go install ./cmd/vega
working-directory: vega
######
## Start capsule
######
@@ -9,14 +9,14 @@ describe('market selector', () => {
});
cy.visit('/markets');
cy.wait('@gqlSimpleMarketsQuery').then((response) => {
if (response.response?.body?.data?.markets?.length) {
markets = response.response?.body?.data?.markets;
if (response.response.body.data?.markets?.length) {
markets = response.response.body.data.markets;
}
});
});
it('should be properly rendered', () => {
if (markets) {
if (markets?.length) {
cy.visit(`/trading/${markets[0].id}`);
cy.get('input[placeholder="Search"]').should(
'have.value',
@@ -27,20 +27,25 @@ describe('market selector', () => {
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.first()
.should('contain.text', markets[0].name);
cy.getByTestId('market-pane').children().find('[role="button"]').click();
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.first()
.click();
cy.getByTestId('market-pane').should('not.be.visible');
}
});
it('typing should change list', () => {
if (markets) {
if (markets?.length) {
cy.visit(`/trading/${markets[0].id}`);
cy.get('input[placeholder="Search"]').type('{backspace}');
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.should('have.length', 1);
.should('have.length.at.least', 1);
cy.get('input[placeholder="Search"]').clear();
cy.get('input[placeholder="Search"]').type('app');
const filtered = markets.filter((market) => market.name.match(/app/i));
@@ -65,7 +70,7 @@ describe('market selector', () => {
});
it('mobile view', () => {
if (markets) {
if (markets?.length) {
cy.viewport('iphone-xr');
cy.visit(`/trading/${markets[0].id}`);
cy.get('[role="dialog"]').should('not.exist');
@@ -0,0 +1,67 @@
import { aliasQuery } from '@vegaprotocol/cypress';
import { generateSimpleMarkets } from '../support/mocks/generate-markets';
import { generateDealTicket } from '../support/mocks/generate-deal-ticket';
describe('Market trade', () => {
let markets;
beforeEach(() => {
cy.mockGQL((req) => {
aliasQuery(req, 'SimpleMarkets', generateSimpleMarkets());
aliasQuery(req, 'DealTicketQuery', generateDealTicket());
});
cy.visit('/markets');
cy.wait('@SimpleMarkets').then((response) => {
if (response.response.body.data?.markets?.length) {
markets = response.response.body.data.markets;
}
});
});
it('side selector should work well', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[0].id}`);
cy.get('#step-1-control [aria-label^="Selected value"]').should(
'have.text',
'Long'
);
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-1-control [aria-label^="Selected value"]').should(
'have.text',
'Short'
);
}
});
it('mobile view should work well', () => {
if (markets?.length) {
cy.viewport('iphone-xr');
cy.visit(`/trading/${markets[0].id}`);
cy.getByTestId('next-button').scrollIntoView().click();
cy.get('button[aria-label="Open long position"]').should(
'have.class',
'selected'
);
cy.get('button[aria-label="Open short position"]').should(
'not.have.class',
'selected'
);
cy.get('button[aria-label="Open short position"]').click();
cy.get('button[aria-label="Open long position"]').should(
'not.have.class',
'selected'
);
cy.get('button[aria-label="Open short position"]').should(
'have.class',
'selected'
);
cy.getByTestId('next-button').scrollIntoView().click();
cy.get('#step-1-control').should(
'contain.html',
'aria-label="Selected value Short"'
);
}
});
});
@@ -0,0 +1,33 @@
export const generateDealTicket = () => {
return {
market: {
id: 'first-btcusd-id',
name: 'AAVEDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
positionDecimalPlaces: 0,
state: 'Active',
tradingMode: 'Continuous',
tradableInstrument: {
instrument: {
product: {
quoteName: 'DAI',
settlementAsset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
name: 'tDAI TEST',
__typename: 'Asset',
},
__typename: 'Future',
},
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
depth: {
lastTrade: { price: '9893006', __typename: 'Trade' },
__typename: 'MarketDepth',
},
__typename: 'Market',
},
};
};
File diff suppressed because it is too large Load Diff
@@ -3,11 +3,7 @@ import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import {
SideSelector,
DealTicketAmount,
MarketSelector,
} from '@vegaprotocol/deal-ticket';
import { DealTicketAmount, MarketSelector } from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
@@ -19,6 +15,7 @@ import {
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';
import SideSelector, { SIDE_NAMES } from './side-selector';
interface DealTicketMarketProps {
market: DealTicketQuery_market;
@@ -47,6 +44,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const orderSide = watch('side');
const { message: invalidText, isDisabled } = useOrderValidation({
step,
@@ -96,6 +94,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
)}
/>
),
value: SIDE_NAMES[orderSide] || '',
},
{
label: t('Choose Position Size'),
@@ -0,0 +1,58 @@
import React from 'react';
import classNames from 'classnames';
import { FormGroup, Button } from '@vegaprotocol/ui-toolkit';
import { VegaWalletOrderSide } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/react-helpers';
interface SideSelectorProps {
value: VegaWalletOrderSide;
onSelect: (side: VegaWalletOrderSide) => void;
}
export const SIDE_NAMES: Record<VegaWalletOrderSide, string> = {
[VegaWalletOrderSide.Buy]: t('Long'),
[VegaWalletOrderSide.Sell]: t('Short'),
};
export default ({ value, onSelect }: SideSelectorProps) => {
return (
<FormGroup
label={t('Direction')}
labelFor="order-side-toggle"
labelClassName="sr-only"
>
<fieldset
className="w-full grid md:grid-cols-2 gap-20"
id="order-side-toggle"
>
<Button
variant="inline-link"
aria-label={t('Open long position')}
className={classNames(
'buyButton hover:buyButton dark:buyButtonDark dark:hover:buyButtonDark',
{ selected: value === VegaWalletOrderSide.Buy }
)}
onClick={() => onSelect(VegaWalletOrderSide.Buy)}
>
{t('Long')}
</Button>
<Button
variant="inline-link"
aria-label={t('Open short position')}
className={classNames(
'sellButton hover:sellButton dark:sellButtonDark dark:hover:sellButtonDark',
{ selected: value === VegaWalletOrderSide.Sell }
)}
onClick={() => onSelect(VegaWalletOrderSide.Sell)}
>
{t('Short')}
</Button>
<div className="md:col-span-2 text-black dark:text-white text-ui-small">
{t(
'Trading derivatives allows you to make a profit or loss regardless of whether the market you are trading goes up or down. If you open a "long" position, you will make a profit if the price of your chosen market goes up, and you will make a profit for "short" positions when the price goes down.'
)}
</div>
</fieldset>
</FormGroup>
);
};
@@ -66,7 +66,7 @@ const SimpleMarketList = () => {
);
const handleOnGridReady = useCallback(() => {
gridRef.current?.api.sizeColumnsToFit();
gridRef.current?.api?.sizeColumnsToFit();
}, [gridRef]);
useEffect(() => {
@@ -146,6 +146,7 @@ export const Stepper = ({ steps }: StepperProps) => {
variant="secondary"
onClick={handleNext}
disabled={steps[activeStep].disabled}
data-testid="next-button"
>
{t('Next')}
</Button>
+150 -36
View File
@@ -71,7 +71,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "119441.92040672521249473",
"locked_amount": "119027.03701568518786269",
"deposits": [
{
"amount": "129999.45",
@@ -521,7 +521,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "97499.58",
"total_removed": "0",
"locked_amount": "60383.599023737078168034",
"locked_amount": "59976.637065262864166124",
"deposits": [
{
"amount": "97499.58",
@@ -554,7 +554,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "135173.4239508",
"total_removed": "0",
"locked_amount": "82533.95507197524777065747004",
"locked_amount": "81977.70833379207840893149176",
"deposits": [
{
"amount": "135173.4239508",
@@ -587,7 +587,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "32499.86",
"total_removed": "0",
"locked_amount": "25402.337340307062040828",
"locked_amount": "25231.135472234075823402",
"deposits": [
{
"amount": "32499.86",
@@ -620,7 +620,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "10833.29",
"total_removed": "0",
"locked_amount": "8268.214305823120249458",
"locked_amount": "8212.489759068967612383",
"deposits": [
{
"amount": "10833.29",
@@ -708,7 +708,7 @@
"tranche_end": "2022-11-01T00:00:00.000Z",
"total_added": "22500",
"total_removed": "0",
"locked_amount": "12778.014605978259",
"locked_amount": "12564.154495018116",
"deposits": [
{
"amount": "15000",
@@ -794,7 +794,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "179856.049568108351",
"locked_amount": "1687449.432596807498595158",
"locked_amount": "1678154.250016267696756174",
"deposits": [
{
"amount": "1852091.69",
@@ -1846,7 +1846,7 @@
"tranche_end": "2022-09-30T00:00:00.000Z",
"total_added": "60916.66666633337",
"total_removed": "19238.601152747179372649",
"locked_amount": "11323.5904706170555036496913660485",
"locked_amount": "11050.4181974506175267769666097872",
"deposits": [
{
"amount": "2833.333333",
@@ -5417,7 +5417,7 @@
"tranche_end": "2022-09-03T00:00:00.000Z",
"total_added": "24801.000000000000000003",
"total_removed": "7821.05162209938",
"locked_amount": "3091.34367180365285328037393778538812784",
"locked_amount": "2972.50947897640675449035956326103500747",
"deposits": [
{
"amount": "25",
@@ -16066,7 +16066,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "144843.5352821052414",
"locked_amount": "2617523.5951378898883575804",
"locked_amount": "2603240.13575045129543246595",
"deposits": [
{
"amount": "1998.95815",
@@ -16811,8 +16811,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15788853.065470999700000001",
"total_removed": "65011.13698690449648",
"locked_amount": "14506606.9984101204011954329757304703081914",
"total_removed": "65454.451699833603555",
"locked_amount": "14456218.0706074620330102207620852061057042",
"deposits": [
{
"amount": "16249.93",
@@ -17386,6 +17386,16 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xa81f6d3ae2ad8a5ef7ef669952bae09fd41bff79c920749a711353a9fc36fad9"
},
{
"amount": "227.6785943833896",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xe11650c233bf405e04b6087b25afcb96ef70e476200328345729f87af5c718eb"
},
{
"amount": "215.636118545717475",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xb305732b2d00b6af088d3b5f48620b1ac92ee9687167b3ab3d97a9a95c9344d6"
},
{
"amount": "579.636872035866225",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -17618,6 +17628,18 @@
"tranche_id": 2,
"tx": "0xa81f6d3ae2ad8a5ef7ef669952bae09fd41bff79c920749a711353a9fc36fad9"
},
{
"amount": "227.6785943833896",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xe11650c233bf405e04b6087b25afcb96ef70e476200328345729f87af5c718eb"
},
{
"amount": "215.636118545717475",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xb305732b2d00b6af088d3b5f48620b1ac92ee9687167b3ab3d97a9a95c9344d6"
},
{
"amount": "579.636872035866225",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -17758,8 +17780,8 @@
}
],
"total_tokens": "194999.1675",
"withdrawn_tokens": "15773.5485219938148",
"remaining_tokens": "179225.6189780061852"
"withdrawn_tokens": "16216.863234922921875",
"remaining_tokens": "178782.304265077078125"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -19198,8 +19220,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "2417337.516236494402292047",
"locked_amount": "7739879.23520260267374320498368782",
"total_removed": "2418281.701339586123510047",
"locked_amount": "7693121.11274897416980976059276093",
"deposits": [
{
"amount": "129284.449",
@@ -19518,6 +19540,21 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xb6a67f32be39bfdb9ada605c4a9ad940eca7d8a1c0573733f987ad2ebad4ffd6"
},
{
"amount": "421.61288895655100925",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xcb6935b6ef6fc175efbeda721ef7b1a443763afd5099a5a274a4bc97a27b74aa"
},
{
"amount": "398.35616080391488175",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x8c9968a8228537544f18188becde255515115ec9aaeea5846281aff807904ab5"
},
{
"amount": "124.216053331255327",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xcf8cdc81ee193ba4bce80db2062d482669154ae4ec5d80ddf4a475d20e32ed9e"
},
{
"amount": "509.31853983395369725",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -21288,6 +21325,24 @@
"tranche_id": 3,
"tx": "0xb6a67f32be39bfdb9ada605c4a9ad940eca7d8a1c0573733f987ad2ebad4ffd6"
},
{
"amount": "421.61288895655100925",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0xcb6935b6ef6fc175efbeda721ef7b1a443763afd5099a5a274a4bc97a27b74aa"
},
{
"amount": "398.35616080391488175",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x8c9968a8228537544f18188becde255515115ec9aaeea5846281aff807904ab5"
},
{
"amount": "124.216053331255327",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0xcf8cdc81ee193ba4bce80db2062d482669154ae4ec5d80ddf4a475d20e32ed9e"
},
{
"amount": "509.31853983395369725",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -22634,8 +22689,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "168594.53928791585959575",
"remaining_tokens": "190528.93028708414040425"
"withdrawn_tokens": "169538.72439100758081375",
"remaining_tokens": "189584.74518399241918625"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -23699,7 +23754,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "5778205.3912159303",
"total_removed": "1576644.495385096585953516",
"locked_amount": "2741169.69268825216016768844203158",
"locked_amount": "2722695.275716053465499410185669762",
"deposits": [
{
"amount": "552496.6455",
@@ -24931,8 +24986,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "423.0179607209",
"locked_amount": "414761.57605235507102892360121764",
"total_removed": "447.9623988229",
"locked_amount": "412498.28026468064445331084830036",
"deposits": [
{
"amount": "3000",
@@ -31581,6 +31636,11 @@
"user": "0x7345AaA0D0e4C4A46d921fA9323973956F2b7392",
"tx": "0x3a09ab938efeceee72457dc54bbfacc522155a7dcd126f7ccdff28110dce5fbc"
},
{
"amount": "24.944438102",
"user": "0x8c951C54F9cd08Cf81F770248e835D7A4F491a46",
"tx": "0x3c8ec5a3dfac440713b7f8808731c216f62df7ec02fdce5a730ef2c7e89abbd8"
},
{
"amount": "11.163032724",
"user": "0xF5037DDA4A660d67560200f45380FF8364e35540",
@@ -48664,10 +48724,17 @@
"tx": "0xc8541da6a57f410b6faba47a5e5184bae700193b7bd042914fffc562114d92f5"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "24.944438102",
"user": "0x8c951C54F9cd08Cf81F770248e835D7A4F491a46",
"tranche_id": 5,
"tx": "0x3c8ec5a3dfac440713b7f8808731c216f62df7ec02fdce5a730ef2c7e89abbd8"
}
],
"total_tokens": "200",
"withdrawn_tokens": "0",
"remaining_tokens": "200"
"withdrawn_tokens": "24.944438102",
"remaining_tokens": "175.055561898"
},
{
"address": "0x3437f6ED844013852650851D769ae7939B5e0aec",
@@ -50742,7 +50809,7 @@
"tranche_start": "2021-12-05T00:00:00.000Z",
"tranche_end": "2022-06-05T00:00:00.000Z",
"total_added": "171288.42",
"total_removed": "39426.8566666082989",
"total_removed": "39740.6212136932989",
"locked_amount": "0",
"deposits": [
{
@@ -55097,6 +55164,26 @@
"user": "0x075A6AF774C9Ef7315879231730916ceD13e84f8",
"tx": "0xcb7f5ec8fed46ee245bd944823e3e314d6f7689d960e5971ebd5ade821313418"
},
{
"amount": "250",
"user": "0xED380970F4f0746C56C033f00507634a985ec330",
"tx": "0xe1eedaec4d14343fccc7a145fc5d26014dbd7eb6e9dd41c4c6c454b0dfff9c13"
},
{
"amount": "3.764547085",
"user": "0x27049a430Df8b89Ae4f899b0383B0C876F9cAcEb",
"tx": "0x27055acf670d336b247b0abb16a5a9809e12aad5d96338ef8c7ecd4b8ea94db8"
},
{
"amount": "40",
"user": "0x8c951C54F9cd08Cf81F770248e835D7A4F491a46",
"tx": "0x8d4388a2dfad5249133cad6c48669948cad556fa44f1ad4ebcc9003cc5c768e9"
},
{
"amount": "20",
"user": "0x709565FbAe7Df190b8f7FC955E32aB467256fE47",
"tx": "0x2d9dbd0c7de15499e328f933cd278023bf92701bc7d366abc59fee1da0e9f6d8"
},
{
"amount": "60.4448387275",
"user": "0xEe3183EcE9ee7d73Fb7bA7F4eB262A2dE68C42B0",
@@ -64472,6 +64559,12 @@
}
],
"withdrawals": [
{
"amount": "3.764547085",
"user": "0x27049a430Df8b89Ae4f899b0383B0C876F9cAcEb",
"tranche_id": 6,
"tx": "0x27055acf670d336b247b0abb16a5a9809e12aad5d96338ef8c7ecd4b8ea94db8"
},
{
"amount": "38.3342872425",
"user": "0x27049a430Df8b89Ae4f899b0383B0C876F9cAcEb",
@@ -64510,8 +64603,8 @@
}
],
"total_tokens": "250",
"withdrawn_tokens": "246.235452915",
"remaining_tokens": "3.764547085"
"withdrawn_tokens": "250",
"remaining_tokens": "0"
},
{
"address": "0x42bC480928828C57c39649A7D10e41227b6d5E4F",
@@ -66011,10 +66104,17 @@
"tx": "0x9f916cf09e8a3c4ade0ffce5190db464d0a2b1dadba78e1ee7ba5d6e751d6148"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "20",
"user": "0x709565FbAe7Df190b8f7FC955E32aB467256fE47",
"tranche_id": 6,
"tx": "0x2d9dbd0c7de15499e328f933cd278023bf92701bc7d366abc59fee1da0e9f6d8"
}
],
"total_tokens": "20",
"withdrawn_tokens": "0",
"remaining_tokens": "20"
"withdrawn_tokens": "20",
"remaining_tokens": "0"
},
{
"address": "0xA9848843a69D535404cF6E7017D563E1AB7d627A",
@@ -69141,10 +69241,17 @@
"tx": "0xc8541da6a57f410b6faba47a5e5184bae700193b7bd042914fffc562114d92f5"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "40",
"user": "0x8c951C54F9cd08Cf81F770248e835D7A4F491a46",
"tranche_id": 6,
"tx": "0x8d4388a2dfad5249133cad6c48669948cad556fa44f1ad4ebcc9003cc5c768e9"
}
],
"total_tokens": "40",
"withdrawn_tokens": "0",
"remaining_tokens": "40"
"withdrawn_tokens": "40",
"remaining_tokens": "0"
},
{
"address": "0xc612391BD2bc1173BF969167d0c71DC6E3E434AE",
@@ -69726,10 +69833,17 @@
"tx": "0xb59405747c8088945a412703637a7b422f3639439ec2ee15e180c0a2a0d71ee4"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "250",
"user": "0xED380970F4f0746C56C033f00507634a985ec330",
"tranche_id": 6,
"tx": "0xe1eedaec4d14343fccc7a145fc5d26014dbd7eb6e9dd41c4c6c454b0dfff9c13"
}
],
"total_tokens": "250",
"withdrawn_tokens": "0",
"remaining_tokens": "250"
"withdrawn_tokens": "250",
"remaining_tokens": "0"
},
{
"address": "0xe8F33102aDD808E841268E4161326C76A3D31d24",
@@ -38,7 +38,7 @@
"tranche_end": "2022-11-26T13:48:10.000Z",
"total_added": "100",
"total_removed": "0",
"locked_amount": "35.63588280060883",
"locked_amount": "35.156712962962966",
"deposits": [
{
"amount": "100",
@@ -242,7 +242,7 @@
"tranche_end": "2022-10-12T00:53:20.000Z",
"total_added": "1100",
"total_removed": "673.04388635",
"locked_amount": "254.756665398274974",
"locked_amount": "249.485797184170459",
"deposits": [
{
"amount": "1000",
+1 -1
View File
@@ -69,7 +69,7 @@
"tranche_end": "2022-10-12T00:53:20.000Z",
"total_added": "1010.000000000000000001",
"total_removed": "668.4622323651",
"locked_amount": "233.91293822932520340023159696854388634",
"locked_amount": "229.07332286910196690022680527016742769",
"deposits": [
{
"amount": "1000",
@@ -75,10 +75,7 @@ it('Update network', () => {
it('Freeform network', () => {
const name = getProposalName({
...proposal,
rationale: {
...proposal.rationale,
hash: '0x0',
},
id: 'test-id',
terms: {
...proposal.terms,
change: {
@@ -86,7 +83,7 @@ it('Freeform network', () => {
},
},
});
expect(name).toEqual('Freeform: 0x0');
expect(name).toEqual('Freeform: test-id');
});
it("Renders unknown proposal if it's a different proposal type", () => {
+1 -1
View File
@@ -12,7 +12,7 @@ export function getProposalName(proposal: Proposals_proposals) {
} else if (change.__typename === 'UpdateNetworkParameter') {
return `Update Network: ${change.networkParameter.key}`;
} else if (change.__typename === 'NewFreeform') {
return `Freeform: ${proposal.rationale.hash}`;
return `Freeform: ${proposal.id}`;
}
return 'Unknown Proposal';
@@ -9,29 +9,6 @@ import { ProposalState, ProposalRejectionReason, VoteValue } from "@vegaprotocol
// GraphQL fragment: ProposalFields
// ====================================================
export interface ProposalFields_rationale {
__typename: "ProposalRationale";
/**
* Link to a text file describing the proposal in depth.
* Optional except for FreeFrom proposal where it's mandatory.
* If set, the `url` property must be set.
*/
url: string | null;
/**
* Description to show a short title / something in case the link goes offline.
* This is to be between 0 and 1024 unicode characters.
* This is mandatory for all proposal.
*/
description: string;
/**
* Cryptographically secure hash (SHA3-512) of the text pointed by the `url` property
* so that viewers can check that the text hasn't been changed over time.
* Optional except for FreeFrom proposal where it's mandatory.
* If set, the `url` property must be set.
*/
hash: string | null;
}
export interface ProposalFields_party {
__typename: "Party";
/**
@@ -282,10 +259,6 @@ export interface ProposalFields {
* Error details of the rejectionReason
*/
errorDetails: string | null;
/**
* Rationale behind the proposal
*/
rationale: ProposalFields_rationale;
/**
* Party that prepared the proposal
*/
@@ -8,11 +8,6 @@ export const PROPOSALS_FRAGMENT = gql`
datetime
rejectionReason
errorDetails
rationale {
url
description
hash
}
party {
id
}
@@ -9,29 +9,6 @@ import { ProposalState, ProposalRejectionReason, VoteValue } from "@vegaprotocol
// GraphQL query operation: Proposal
// ====================================================
export interface Proposal_proposal_rationale {
__typename: "ProposalRationale";
/**
* Link to a text file describing the proposal in depth.
* Optional except for FreeFrom proposal where it's mandatory.
* If set, the `url` property must be set.
*/
url: string | null;
/**
* Description to show a short title / something in case the link goes offline.
* This is to be between 0 and 1024 unicode characters.
* This is mandatory for all proposal.
*/
description: string;
/**
* Cryptographically secure hash (SHA3-512) of the text pointed by the `url` property
* so that viewers can check that the text hasn't been changed over time.
* Optional except for FreeFrom proposal where it's mandatory.
* If set, the `url` property must be set.
*/
hash: string | null;
}
export interface Proposal_proposal_party {
__typename: "Party";
/**
@@ -282,10 +259,6 @@ export interface Proposal_proposal {
* Error details of the rejectionReason
*/
errorDetails: string | null;
/**
* Rationale behind the proposal
*/
rationale: Proposal_proposal_rationale;
/**
* Party that prepared the proposal
*/
@@ -9,29 +9,6 @@ import { ProposalState, ProposalRejectionReason, VoteValue } from "@vegaprotocol
// GraphQL query operation: Proposals
// ====================================================
export interface Proposals_proposals_rationale {
__typename: "ProposalRationale";
/**
* Link to a text file describing the proposal in depth.
* Optional except for FreeFrom proposal where it's mandatory.
* If set, the `url` property must be set.
*/
url: string | null;
/**
* Description to show a short title / something in case the link goes offline.
* This is to be between 0 and 1024 unicode characters.
* This is mandatory for all proposal.
*/
description: string;
/**
* Cryptographically secure hash (SHA3-512) of the text pointed by the `url` property
* so that viewers can check that the text hasn't been changed over time.
* Optional except for FreeFrom proposal where it's mandatory.
* If set, the `url` property must be set.
*/
hash: string | null;
}
export interface Proposals_proposals_party {
__typename: "Party";
/**
@@ -282,10 +259,6 @@ export interface Proposals_proposals {
* Error details of the rejectionReason
*/
errorDetails: string | null;
/**
* Rationale behind the proposal
*/
rationale: Proposals_proposals_rationale;
/**
* Party that prepared the proposal
*/
@@ -24,12 +24,6 @@ export function generateProposal(
__typename: 'Party',
id: faker.datatype.uuid(),
},
rationale: {
__typename: 'ProposalRationale',
hash: faker.datatype.uuid(),
url: faker.internet.url(),
description: faker.lorem.words(),
},
terms: {
__typename: 'ProposalTerms',
closingDatetime:
@@ -53,6 +53,6 @@ describe('deposit form validation', () => {
.clear()
.type('100')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Amount is above approved amount');
.should('have.text', 'Insufficient amount in Ethereum wallet');
});
});
@@ -11,6 +11,7 @@ describe('withdraw', () => {
const amountField = 'input[name="amount"]';
const useMaximumAmount = 'use-maximum';
const submitWithdrawBtn = 'submit-withdrawal';
const ethAddressValue = Cypress.env('ETHEREUM_WALLET_ADDRESS');
beforeEach(() => {
cy.mockWeb3Provider();
@@ -41,12 +42,8 @@ describe('withdraw', () => {
// only 2 despite 3 fields because the ethereum address will be auto populated
cy.getByTestId(formFieldError).should('have.length', 2);
// Test for invalid Ethereum address
cy.get(toAddressField)
.clear()
.type('invalid-ethereum-address')
.next('[data-testid="input-error-text"]')
.should('contain.text', 'Invalid Ethereum address');
// Test for Ethereum address
cy.get(toAddressField).should('have.value', ethAddressValue);
// Test min amount
cy.get(assetSelectField).select('Asset 1'); // Select asset so we have a min viable amount calculated
@@ -61,7 +58,7 @@ describe('withdraw', () => {
.clear()
.type('1') // Will be above maximum because the vega wallet doesnt have any collateral
.next('[data-testid="input-error-text"]')
.should('contain.text', 'Value is above maximum');
.should('contain.text', 'Insufficient amount in account');
});
it('can set amount using use maximum button', () => {
@@ -51,7 +51,6 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
id: 'buyer-id',
tradesConnection: {
__typename: 'TradeConnection',
totalCount: 1,
edges: fills.map((f) => {
return {
__typename: 'TradeEdge',
@@ -63,6 +62,8 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
__typename: 'PageInfo',
startCursor: '1',
endCursor: '2',
hasNextPage: false,
hasPreviousPage: false,
},
},
__typename: 'Party',
@@ -1,9 +1,12 @@
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
import type { Trades, Trades_market_trades } from '@vegaprotocol/trades';
import type {
Trades,
Trades_market_tradesConnection_edges_node,
} from '@vegaprotocol/trades';
export const generateTrades = (override?: PartialDeep<Trades>): Trades => {
const trades: Trades_market_trades[] = [
const trades: Trades_market_tradesConnection_edges_node[] = [
{
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
@@ -44,10 +47,26 @@ export const generateTrades = (override?: PartialDeep<Trades>): Trades => {
__typename: 'Trade',
},
];
const defaultResult = {
const defaultResult: Trades = {
market: {
id: 'market-0',
trades,
tradesConnection: {
__typename: 'TradeConnection',
edges: trades.map((node, i) => {
return {
__typename: 'TradeEdge',
node,
cursor: (i + 1).toString(),
};
}),
pageInfo: {
__typename: 'PageInfo',
startCursor: '0',
endCursor: trades.length.toString(),
hasNextPage: false,
hasPreviousPage: false,
},
},
__typename: 'Market',
},
};
+6 -9
View File
@@ -1,5 +1,4 @@
import { gql, useQuery } from '@apollo/client';
import { MarketTradingMode } from '@vegaprotocol/types';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import orderBy from 'lodash/orderBy';
import { useRouter } from 'next/router';
@@ -20,15 +19,13 @@ const MARKETS_QUERY = gql`
}
`;
const marketList = ({ markets }: MarketsLanding) =>
orderBy(
markets?.filter(
({ marketTimestamps, tradingMode }) =>
marketTimestamps.open && tradingMode === MarketTradingMode.Continuous
) || [],
['state', 'marketTimestamps.open', 'id'],
const getMarketList = ({ markets = [] }: MarketsLanding) => {
return orderBy(
markets,
['marketTimestamps.open', 'id'],
['asc', 'asc', 'asc']
);
};
export function Index() {
const { replace } = useRouter();
@@ -39,7 +36,7 @@ export function Index() {
useEffect(() => {
if (data) {
const marketId = marketList(data)[0]?.id;
const marketId = getMarketList(data)[0]?.id;
// If a default market is found, go to it with the landing dialog open
if (marketId) {
@@ -41,6 +41,16 @@ const MARKET_QUERY = gql`
metadata {
tags
}
product {
... on Future {
quoteName
settlementAsset {
id
symbol
name
}
}
}
}
}
marketTimestamps {
+32
View File
@@ -61,6 +61,34 @@ export interface Market_market_tradableInstrument_instrument_metadata {
tags: string[] | null;
}
export interface Market_market_tradableInstrument_instrument_product_settlementAsset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
/**
* The full name of the asset (e.g: Great British Pound)
*/
name: string;
}
export interface Market_market_tradableInstrument_instrument_product {
__typename: "Future";
/**
* String representing the quote (e.g. BTCUSD -> USD is quote)
*/
quoteName: string;
/**
* The name of the asset (string)
*/
settlementAsset: Market_market_tradableInstrument_instrument_product_settlementAsset;
}
export interface Market_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
@@ -75,6 +103,10 @@ export interface Market_market_tradableInstrument_instrument {
* Metadata for this instrument
*/
metadata: Market_market_tradableInstrument_instrument_metadata;
/**
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
*/
product: Market_market_tradableInstrument_instrument_product;
}
export interface Market_market_tradableInstrument {
+34 -12
View File
@@ -65,7 +65,7 @@ export const TradeMarketHeader = ({
const itemValueClassName =
'font-sans tracking-tighter text-black dark:text-white text-ui';
const headerClassName = classNames(
'w-full p-8 bg-white dark:bg-black',
'w-full p-8 mb-4 bg-white dark:bg-black',
className
);
return (
@@ -85,14 +85,14 @@ export const TradeMarketHeader = ({
className="flex flex-auto items-start gap-64 overflow-x-auto whitespace-nowrap"
>
<div className={headerItemClassName}>
<span className={itemClassName}>Change (24h)</span>
<span className={itemClassName}>{t('Change (24h)')}</span>
<PriceCellChange
candles={candlesClose}
decimalPlaces={market.decimalPlaces}
/>
</div>
<div className={headerItemClassName}>
<span className={itemClassName}>Volume</span>
<span className={itemClassName}>{t('Volume')}</span>
<span data-testid="trading-volume" className={itemValueClassName}>
{market.data && market.data.indicativeVolume !== '0'
? addDecimalsFormatNumber(
@@ -103,7 +103,7 @@ export const TradeMarketHeader = ({
</span>
</div>
<div className={headerItemClassName}>
<span className={itemClassName}>Trading mode</span>
<span className={itemClassName}>{t('Trading mode')}</span>
<span data-testid="trading-mode" className={itemValueClassName}>
{market.tradingMode === MarketTradingMode.MonitoringAuction &&
market.data?.trigger &&
@@ -114,6 +114,29 @@ export const TradeMarketHeader = ({
: formatLabel(market.tradingMode)}
</span>
</div>
<div className={headerItemClassName}>
<span className={itemClassName}>{t('Price')}</span>
<span data-testid="mark-price" className={itemValueClassName}>
{market.data && market.data.markPrice !== '0'
? addDecimalsFormatNumber(
market.data.markPrice,
market.decimalPlaces
)
: '-'}
</span>
</div>
{market.tradableInstrument.instrument.product?.settlementAsset
?.symbol && (
<div className={headerItemClassName}>
<span className={itemClassName}>{t('Settlement asset')}</span>
<span data-testid="trading-mode" className={itemValueClassName}>
{
market.tradableInstrument.instrument.product?.settlementAsset
?.symbol
}
</span>
</div>
)}
</div>
</div>
</header>
@@ -140,7 +163,7 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
<Allotment.Pane>
<Allotment proportionalLayout={false} minSize={200}>
<Allotment.Pane priority={LayoutPriority.High} minSize={200}>
<TradeGridChild className="h-full px-4">
<TradeGridChild className="h-full px-4 bg-black-10 dark:bg-black-70">
<Tabs>
<Tab id="candles" name={t('Candles')}>
<TradingViews.Candles marketId={market.id} />
@@ -153,10 +176,10 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
</Allotment.Pane>
<Allotment.Pane
priority={LayoutPriority.Low}
preferredSize={375}
preferredSize={330}
minSize={200}
>
<TradeGridChild className="h-full px-4">
<TradeGridChild className="h-full px-4 bg-black-10 dark:bg-black-70">
<Tabs>
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket marketId={market.id} />
@@ -169,10 +192,10 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
</Allotment.Pane>
<Allotment.Pane
priority={LayoutPriority.Low}
preferredSize={460}
preferredSize={430}
minSize={200}
>
<TradeGridChild className="h-full px-4">
<TradeGridChild className="h-full px-4 bg-black-10 dark:bg-black-70">
<Tabs>
<Tab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={market.id} />
@@ -186,12 +209,11 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
</Allotment>
</Allotment.Pane>
<Allotment.Pane
snap={true}
priority={LayoutPriority.Low}
preferredSize={200}
minSize={200}
minSize={50}
>
<TradeGridChild className="h-full">
<TradeGridChild className="h-full mt-4">
<Tabs>
<Tab id="positions" name={t('Positions')}>
<TradingViews.Positions />
+2
View File
@@ -10,8 +10,10 @@ body,
html > body {
--focus-border: theme('colors.vega.pink');
--separator-border: theme('colors.black.10');
}
html.dark > body {
--focus-border: theme('colors.vega.yellow');
--separator-border: theme('colors.black.70');
}
@@ -3,12 +3,36 @@
// @generated
// This file was automatically generated and should not be edited.
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
import { MarketState, MarketTradingMode, AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: MarketInfoQuery
// ====================================================
export interface MarketInfoQuery_market_accounts_asset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
}
export interface MarketInfoQuery_market_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Asset, the 'currency'
*/
asset: MarketInfoQuery_market_accounts_asset;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
balance: string;
}
export interface MarketInfoQuery_market_fees_factors {
__typename: "FeeFactors";
/**
@@ -125,6 +149,42 @@ export interface MarketInfoQuery_market_data {
* the aggregated volume being offered at the best static offer price, excluding pegged orders.
*/
bestStaticOfferVolume: string;
/**
* the sum of the size of all positions greater than 0.
*/
openInterest: string;
}
export interface MarketInfoQuery_market_liquidityMonitoringParameters_targetStakeParameters {
__typename: "TargetStakeParameters";
/**
* Specifies length of time window expressed in seconds for target stake calculation
*/
timeWindow: number;
/**
* Specifies scaling factors used in target stake calculation
*/
scalingFactor: number;
}
export interface MarketInfoQuery_market_liquidityMonitoringParameters {
__typename: "LiquidityMonitoringParameters";
/**
* Specifies the triggering ratio for entering liquidity auction
*/
triggeringRatio: number;
/**
* Specifies parameters related to target stake calculation
*/
targetStakeParameters: MarketInfoQuery_market_liquidityMonitoringParameters_targetStakeParameters;
}
export interface MarketInfoQuery_market_tradableInstrument_instrument_metadata {
__typename: "InstrumentMetadata";
/**
* An arbitrary list of tags to associated to associate to the Instrument (string list)
*/
tags: string[] | null;
}
export interface MarketInfoQuery_market_tradableInstrument_instrument_product_settlementAsset {
@@ -143,6 +203,28 @@ export interface MarketInfoQuery_market_tradableInstrument_instrument_product_se
name: string;
}
export interface MarketInfoQuery_market_tradableInstrument_instrument_product_oracleSpecForSettlementPrice {
__typename: "OracleSpec";
/**
* id is a hash generated from the OracleSpec data.
*/
id: string;
}
export interface MarketInfoQuery_market_tradableInstrument_instrument_product_oracleSpecForTradingTermination {
__typename: "OracleSpec";
/**
* id is a hash generated from the OracleSpec data.
*/
id: string;
}
export interface MarketInfoQuery_market_tradableInstrument_instrument_product_oracleSpecBinding {
__typename: "OracleSpecToFutureBinding";
settlementPriceProperty: string;
tradingTerminationProperty: string;
}
export interface MarketInfoQuery_market_tradableInstrument_instrument_product {
__typename: "Future";
/**
@@ -153,10 +235,38 @@ export interface MarketInfoQuery_market_tradableInstrument_instrument_product {
* The name of the asset (string)
*/
settlementAsset: MarketInfoQuery_market_tradableInstrument_instrument_product_settlementAsset;
/**
* The oracle spec describing the oracle data of interest for settlement price.
*/
oracleSpecForSettlementPrice: MarketInfoQuery_market_tradableInstrument_instrument_product_oracleSpecForSettlementPrice;
/**
* The oracle spec describing the oracle data of interest for trading termination.
*/
oracleSpecForTradingTermination: MarketInfoQuery_market_tradableInstrument_instrument_product_oracleSpecForTradingTermination;
/**
* The binding between the oracle spec and the settlement price
*/
oracleSpecBinding: MarketInfoQuery_market_tradableInstrument_instrument_product_oracleSpecBinding;
}
export interface MarketInfoQuery_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Uniquely identify an instrument across all instruments available on Vega (string)
*/
id: string;
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
/**
* Metadata for this instrument
*/
metadata: MarketInfoQuery_market_tradableInstrument_instrument_metadata;
/**
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
*/
@@ -258,14 +368,14 @@ export interface MarketInfoQuery_market {
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
@@ -286,6 +396,10 @@ export interface MarketInfoQuery_market {
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
/**
* Get account for a party or market
*/
accounts: MarketInfoQuery_market_accounts[] | null;
/**
* Fees related data
*/
@@ -302,6 +416,10 @@ export interface MarketInfoQuery_market {
* marketData for the given market
*/
data: MarketInfoQuery_market_data | null;
/**
* Liquidity monitoring parameters for the market
*/
liquidityMonitoringParameters: MarketInfoQuery_market_liquidityMonitoringParameters;
/**
* An instance of or reference to a tradable instrument.
*/
+93 -16
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
addDecimalsFormatNumber,
formatLabel,
formatNumber,
formatNumberPercentage,
t,
@@ -15,10 +16,7 @@ import {
import startCase from 'lodash/startCase';
import pick from 'lodash/pick';
import omit from 'lodash/omit';
import type {
MarketInfoQuery,
MarketInfoQuery_market,
} from './__generated__/MarketInfoQuery';
import type { MarketInfoQuery, MarketInfoQuery_market } from './__generated__';
import BigNumber from 'bignumber.js';
import { gql, useQuery } from '@apollo/client';
@@ -31,6 +29,13 @@ const MARKET_INFO_QUERY = gql`
positionDecimalPlaces
state
tradingMode
accounts {
type
asset {
id
}
balance
}
fees {
factors {
makerFee
@@ -53,6 +58,13 @@ const MARKET_INFO_QUERY = gql`
short
long
}
accounts {
type
asset {
id
}
balance
}
data {
market {
id
@@ -64,9 +76,23 @@ const MARKET_INFO_QUERY = gql`
bestStaticBidVolume
bestStaticOfferVolume
indicativeVolume
openInterest
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
}
}
tradableInstrument {
instrument {
id
name
code
metadata {
tags
}
product {
... on Future {
quoteName
@@ -75,6 +101,16 @@ const MARKET_INFO_QUERY = gql`
symbol
name
}
oracleSpecForSettlementPrice {
id
}
oracleSpecForTradingTermination {
id
}
oracleSpecBinding {
settlementPriceProperty
tradingTerminationProperty
}
}
}
}
@@ -159,19 +195,28 @@ export const Info = ({ market }: InfoProps) => {
),
},
];
const keyDetails = pick(
market,
'name',
'decimalPlaces',
'positionDecimalPlaces',
'tradingMode',
'state',
'id' as 'marketId'
);
const marketSpecPanels = [
{
title: t('Key details'),
content: (
<MarketInfoTable
data={pick(
market,
'name',
'decimalPlaces',
'positionDecimalPlaces',
'tradingMode',
'state'
)}
data={{
...keyDetails,
marketId: keyDetails.id,
id: undefined,
tradingMode:
keyDetails.tradingMode && formatLabel(keyDetails.tradingMode),
}}
/>
),
},
@@ -180,8 +225,28 @@ export const Info = ({ market }: InfoProps) => {
content: (
<MarketInfoTable
data={{
product: market.tradableInstrument.instrument.product,
...market.tradableInstrument.instrument.product.settlementAsset,
marketName: market.tradableInstrument.instrument.name,
code: market.tradableInstrument.instrument.code,
productType:
market.tradableInstrument.instrument.product.__typename,
...market.tradableInstrument.instrument.product,
...(market.tradableInstrument.instrument.product?.settlementAsset ??
{}),
}}
/>
),
},
{
title: t('Metadata'),
content: (
<MarketInfoTable
data={{
...market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
const [key, value] = tag.split(':');
return { [key]: value };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
}}
/>
),
@@ -212,6 +277,18 @@ export const Info = ({ market }: InfoProps) => {
content: <MarketInfoTable data={trigger} />,
})
),
{
title: t('Liquidity monitoring parameters'),
content: (
<MarketInfoTable
data={{
triggeringRatio:
market.liquidityMonitoringParameters.triggeringRatio,
...market.liquidityMonitoringParameters.targetStakeParameters,
}}
/>
),
},
];
return (
@@ -261,7 +338,7 @@ const Row = ({
? decimalPlaces
? addDecimalsFormatNumber(value, decimalPlaces)
: asPercentage
? formatNumberPercentage(new BigNumber(value))
? formatNumberPercentage(new BigNumber(value * 100))
: formatNumber(Number(value))
: value}
</KeyValueTableRow>
@@ -283,7 +360,7 @@ export const MarketInfoTable = ({
decimalPlaces,
asPercentage,
unformatted,
omits = ['id', '__typename'],
omits = ['__typename'],
}: MarketInfoTableProps) => {
return (
<KeyValueTable muted={true}>
@@ -203,7 +203,7 @@ export const MarketSelector = ({ market, setMarket, ItemRenderer }: Props) => {
/>
</Button>
</div>
<hr className="md:hidden mb-5" />
<hr className="mb-5" />
<div
className={classNames(
'md:absolute z-20 flex flex-col top-[30px] z-10 md:drop-shadow-md md:border-1 md:border-black md:dark:border-white bg-white dark:bg-black text-black dark:text-white min-w-full md:max-h-[200px] overflow-y-auto',
+210 -158
View File
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { waitFor, fireEvent, render, screen } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import type { DepositFormProps } from './deposit-form';
import { DepositForm } from './deposit-form';
@@ -25,6 +25,7 @@ function generateAsset(): Asset {
let asset: Asset;
let props: DepositFormProps;
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
beforeEach(() => {
asset = generateAsset();
@@ -32,7 +33,7 @@ beforeEach(() => {
assets: [asset],
selectedAsset: undefined,
onSelectAsset: jest.fn(),
available: new BigNumber(5),
balance: new BigNumber(5),
submitApprove: jest.fn(),
submitDeposit: jest.fn(),
requestFaucet: jest.fn(),
@@ -43,171 +44,222 @@ beforeEach(() => {
allowance: new BigNumber(30),
isFaucetable: true,
};
(useVegaWallet as jest.Mock).mockReturnValue({ keypair: null });
(useWeb3React as jest.Mock).mockReturnValue({ account: MOCK_ETH_ADDRESS });
});
it('Form validation', async () => {
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ keypair: null });
describe('Deposit form', () => {
it('renders with default values', async () => {
render(<DepositForm {...props} />);
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account: undefined });
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
expect(screen.getByLabelText('From (Ethereum address)')).toHaveValue(
MOCK_ETH_ADDRESS
);
expect(screen.getByLabelText('Asset')).toHaveValue('');
expect(screen.getByLabelText('To (Vega key)')).toHaveValue('');
expect(screen.getByLabelText('Amount')).toHaveValue(null);
});
const { rerender } = render(<DepositForm {...props} />);
describe('fields validation', () => {
it('fails when submitted with empty required fields', async () => {
render(<DepositForm {...props} />);
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
expect(screen.getByLabelText('From (Ethereum address)')).toHaveValue('');
expect(screen.getByLabelText('Asset')).toHaveValue('');
expect(screen.getByLabelText('To (Vega key)')).toHaveValue('');
expect(screen.getByLabelText('Amount')).toHaveValue(null);
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(props.submitDeposit).not.toHaveBeenCalled();
const validationMessages = await screen.findAllByRole('alert');
expect(validationMessages).toHaveLength(3);
validationMessages.forEach((el) => {
expect(el).toHaveTextContent('Required');
});
});
it('fails when submitted with invalid ethereum address', async () => {
(useWeb3React as jest.Mock).mockReturnValue({ account: '123' });
render(<DepositForm {...props} />);
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Invalid Ethereum address')
).toBeInTheDocument();
});
it('fails when submitted with invalid vega wallet key', async () => {
render(<DepositForm {...props} />);
const invalidVegaKey = 'abc';
fireEvent.change(screen.getByLabelText('To (Vega key)'), {
target: { value: invalidVegaKey },
});
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(await screen.findByText('Invalid Vega key')).toBeInTheDocument();
});
it('fails when submitted amount is more than the amount available in the ethereum wallet', async () => {
render(<DepositForm {...props} />);
// Max amount validation
const amountMoreThanAvailable = '7';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountMoreThanAvailable },
});
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Insufficient amount in Ethereum wallet')
).toBeInTheDocument();
});
it('fails when submitted amount is more than the maximum limit', async () => {
render(<DepositForm {...props} />);
const amountMoreThanLimit = '21';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountMoreThanLimit },
});
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Insufficient amount in Ethereum wallet')
).toBeInTheDocument();
});
it('fails when submitted amount is more than the approved amount', async () => {
render(
<DepositForm
{...props}
balance={new BigNumber(100)}
limits={{ max: new BigNumber(100), deposited: new BigNumber(10) }}
/>
);
const amountMoreThanAllowance = '31';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountMoreThanAllowance },
});
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above approved amount')
).toBeInTheDocument();
});
it('fails when submitted amount is less than the minimum limit', async () => {
// Min amount validation
render(<DepositForm {...props} selectedAsset={asset} />); // Render with selected asset so we have asset.decimals
const amountLessThanMinViable = '0.00001';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountLessThanMinViable },
});
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
});
it('fails when submitted amount is less than zero', async () => {
render(<DepositForm {...props} />);
const amountLessThanZero = '-0.00001';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountLessThanZero },
});
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
});
});
it('handles deposit approvals', () => {
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ keypair: null });
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account: undefined });
render(
<DepositForm
{...props}
allowance={new BigNumber(0)}
selectedAsset={asset}
/>
);
fireEvent.click(
screen.getByText(`Approve ${asset.symbol}`, {
selector: '[type="button"]',
})
);
expect(props.submitApprove).toHaveBeenCalled();
});
it('handles submitting a deposit', async () => {
const vegaKey =
'f8885edfa7ffdb6ed996ca912e9258998e47bf3515c885cf3c63fb56b15de36f';
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ keypair: { pub: vegaKey } });
const account = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account });
const limits = {
max: new BigNumber(20),
deposited: new BigNumber(10),
};
const balance = new BigNumber(50);
render(
<DepositForm
{...props}
allowance={new BigNumber(100)}
balance={balance}
limits={limits}
selectedAsset={asset}
/>
);
// Check deposit limit is displayed
expect(
screen.getByText('Balance available', { selector: 'th' })
.nextElementSibling
).toHaveTextContent(balance.toString());
expect(
screen.getByText('Maximum total deposit amount', { selector: 'th' })
.nextElementSibling
).toHaveTextContent(limits.max.toString());
expect(
screen.getByText('Deposited', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.deposited.toString());
expect(
screen.getByText('Remaining', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.max.minus(limits.deposited).toString());
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '8' },
});
await act(async () => {
fireEvent.click(
screen.getByText('Deposit', { selector: '[type="submit"]' })
);
});
expect(props.submitDeposit).not.toHaveBeenCalled();
const validationMessages = screen.getAllByRole('alert');
expect(validationMessages).toHaveLength(4);
validationMessages.forEach((el) => {
expect(el).toHaveTextContent('Required');
});
// Address validation
const invalidEthereumAddress = '123';
fireEvent.change(screen.getByLabelText('From (Ethereum address)'), {
target: { value: invalidEthereumAddress },
});
expect(
await screen.findByText('Invalid Ethereum address')
).toBeInTheDocument();
const invalidVegaKey = 'abc';
fireEvent.change(screen.getByLabelText('To (Vega key)'), {
target: { value: invalidVegaKey },
});
expect(await screen.findByText('Invalid Vega key')).toBeInTheDocument();
// Max amount validation
const amountMoreThanAvailable = '7'; // but also less than lifetime limit available
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountMoreThanAvailable },
});
expect(
await screen.findByText('Insufficient amount in Ethereum wallet')
).toBeInTheDocument();
const amountMoreThanLimit = '11';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountMoreThanLimit },
});
expect(
await screen.findByText('Amount is above permitted maximum')
).toBeInTheDocument();
rerender(
<DepositForm
{...props}
limits={{ max: new BigNumber(100), deposited: new BigNumber(10) }}
/>
);
const amountMoreThanAllowance = '31';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountMoreThanAllowance },
});
expect(
await screen.findByText('Amount is above approved amount')
).toBeInTheDocument();
// Min amount validation
rerender(<DepositForm {...props} selectedAsset={asset} />); // Rerender with selected asset so we have asset.decimals
const amountLessThanMinViable = '0.00001';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountLessThanMinViable },
});
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
const amountLessThanZero = '-0.00001';
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amountLessThanZero },
});
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
});
it('Approval', () => {
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ keypair: null });
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account: undefined });
render(
<DepositForm
{...props}
allowance={new BigNumber(0)}
selectedAsset={asset}
/>
);
fireEvent.click(
screen.getByText(`Approve ${asset.symbol}`, { selector: '[type="button"]' })
);
expect(props.submitApprove).toHaveBeenCalled();
});
it('Deposit', async () => {
const vegaKey =
'f8885edfa7ffdb6ed996ca912e9258998e47bf3515c885cf3c63fb56b15de36f';
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ keypair: { pub: vegaKey } });
const account = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account });
const limits = {
max: new BigNumber(20),
deposited: new BigNumber(10),
};
render(
<DepositForm
{...props}
allowance={new BigNumber(100)}
available={new BigNumber(50)}
limits={limits}
selectedAsset={asset}
/>
);
// Check deposit limit is displayed
expect(
screen.getByText('Max deposit total', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.max.toString());
expect(
screen.getByText('Remaining available', { selector: 'th' })
.nextElementSibling
).toHaveTextContent(limits.max.minus(limits.deposited).toString());
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '8' },
});
await act(async () => {
fireEvent.click(
screen.getByText('Deposit', { selector: '[type="submit"]' })
);
});
expect(props.submitDeposit).toHaveBeenCalledWith({
// @ts-ignore contract address definitely defined
assetSource: asset.source.contractAddress,
amount: '800',
vegaPublicKey: vegaKey,
await waitFor(() => {
expect(props.submitDeposit).toHaveBeenCalledWith({
// @ts-ignore contract address definitely defined
assetSource: asset.source.contractAddress,
amount: '800',
vegaPublicKey: vegaKey,
});
});
});
});
+20 -18
View File
@@ -1,7 +1,7 @@
import {
removeDecimal,
t,
ethereumAddress,
t,
required,
vegaPublicKey,
minSafe,
@@ -18,10 +18,10 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
import { Web3WalletInput } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { useEffect } from 'react';
import { useMemo, useEffect } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import type { Asset } from './deposit-manager';
@@ -37,7 +37,7 @@ export interface DepositFormProps {
assets: Asset[];
selectedAsset?: Asset;
onSelectAsset: (assetId: string) => void;
available: BigNumber | undefined;
balance: BigNumber | undefined;
submitApprove: () => Promise<void>;
submitDeposit: (args: {
assetSource: string;
@@ -57,7 +57,7 @@ export const DepositForm = ({
assets,
selectedAsset,
onSelectAsset,
available,
balance,
submitApprove,
submitDeposit,
requestFaucet,
@@ -99,7 +99,7 @@ export const DepositForm = ({
const max = useMemo(() => {
const maxApproved = allowance ? allowance : new BigNumber(0);
const maxAvailable = available ? available : new BigNumber(0);
const maxAvailable = balance ? balance : new BigNumber(0);
// limits.max is a lifetime deposit limit, so the actual max value for form
// input is the max minus whats already been deposited
@@ -116,7 +116,7 @@ export const DepositForm = ({
limit: maxLimit,
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
};
}, [limits, allowance, available]);
}, [limits, allowance, balance]);
const min = useMemo(() => {
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
@@ -141,9 +141,11 @@ export const DepositForm = ({
label={t('From (Ethereum address)')}
labelFor="ethereum-address"
>
<Input
{...register('from', { validate: { required, ethereumAddress } })}
id="ethereum-address"
<Web3WalletInput
inputProps={{
id: 'ethereum-address',
...register('from', { validate: { required, ethereumAddress } }),
}}
/>
{errors.from?.message && (
<InputError intent="danger" className="mt-4">
@@ -196,7 +198,7 @@ export const DepositForm = ({
</FormGroup>
{selectedAsset && limits && (
<div className="mb-20">
<DepositLimits limits={limits} />
<DepositLimits limits={limits} balance={balance} />
</div>
)}
<FormGroup label={t('Amount')} labelFor="amount" className="relative">
@@ -210,12 +212,12 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max.approved)) {
return t('Amount is above approved amount');
} else if (value.isGreaterThan(max.limit)) {
return t('Amount is above permitted maximum');
} else if (value.isGreaterThan(max.available)) {
if (value.isGreaterThan(max.available)) {
return t('Insufficient amount in Ethereum wallet');
} else if (value.isGreaterThan(max.limit)) {
return t('Amount is above temporary deposit limit');
} else if (value.isGreaterThan(max.approved)) {
return t('Amount is above approved amount');
}
return maxSafe(max.amount)(v);
},
@@ -227,10 +229,10 @@ export const DepositForm = ({
{errors.amount.message}
</InputError>
)}
{account && selectedAsset && available && (
{selectedAsset && balance && (
<UseButton
onClick={() => {
setValue('amount', max.amount.toFixed(selectedAsset.decimals));
setValue('amount', balance.toFixed(selectedAsset.decimals));
clearErrors('amount');
}}
>
+31 -25
View File
@@ -6,11 +6,11 @@ interface DepositLimitsProps {
max: BigNumber;
deposited: BigNumber;
};
balance?: BigNumber;
}
export const DepositLimits = ({ limits }: DepositLimitsProps) => {
export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => {
let maxLimit = '';
if (limits.max.isEqualTo(Infinity)) {
maxLimit = t('No limit');
} else if (limits.max.isGreaterThan(1_000_000)) {
@@ -19,29 +19,35 @@ export const DepositLimits = ({ limits }: DepositLimitsProps) => {
maxLimit = limits.max.toString();
}
let remaining = '';
if (limits.deposited.isEqualTo(0)) {
remaining = maxLimit;
} else {
remaining = limits.max.minus(limits.deposited).toString();
}
return (
<>
<p className="text-ui font-bold">{t('Deposit limits')}</p>
<table className="w-full text-ui">
<tbody>
<tr>
<th className="text-left font-normal">{t('Max deposit total')}</th>
<td className="text-right">{maxLimit}</td>
</tr>
<tr>
<th className="text-left font-normal">{t('Deposited')}</th>
<td className="text-right">{limits.deposited.toString()}</td>
</tr>
<tr>
<th className="text-left font-normal">
{t('Remaining available')}
</th>
<td className="text-right">
{limits.max.minus(limits.deposited).toString()}
</td>
</tr>
</tbody>
</table>
</>
<table className="w-full text-ui">
<tbody>
<tr>
<th className="text-left font-normal">{t('Balance available')}</th>
<td className="text-right">{balance ? balance.toString() : 0}</td>
</tr>
<tr>
<th className="text-left font-normal">
{t('Maximum total deposit amount')}
</th>
<td className="text-right">{maxLimit}</td>
</tr>
<tr>
<th className="text-left font-normal">{t('Deposited')}</th>
<td className="text-right">{limits.deposited.toString()}</td>
</tr>
<tr>
<th className="text-left font-normal">{t('Remaining')}</th>
<td className="text-right">{remaining}</td>
</tr>
</tbody>
</table>
);
};
+1 -1
View File
@@ -105,7 +105,7 @@ export const DepositManager = ({
return (
<>
<DepositForm
available={balance}
balance={balance}
selectedAsset={asset}
onSelectAsset={(id) => setAssetId(id)}
assets={sortBy(assets, 'name')}
+2 -4
View File
@@ -206,14 +206,12 @@ export interface Fills_party_tradesConnection_pageInfo {
__typename: "PageInfo";
startCursor: string;
endCursor: string;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
export interface Fills_party_tradesConnection {
__typename: "TradeConnection";
/**
* The total number of trades in this connection
*/
totalCount: number;
/**
* The trade in this connection
*/
+22 -41
View File
@@ -1,11 +1,16 @@
import produce from 'immer';
import orderBy from 'lodash/orderBy';
import { gql } from '@apollo/client';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type { PageInfo, Pagination } from '@vegaprotocol/react-helpers';
import {
makeDataProvider,
defaultAppend as append,
} from '@vegaprotocol/react-helpers';
import type { PageInfo } from '@vegaprotocol/react-helpers';
import type { FillFields } from './__generated__/FillFields';
import type {
Fills,
Fills_party_tradesConnection_edges,
Fills_party_tradesConnection_edges_node,
} from './__generated__/Fills';
import type { FillsSub } from './__generated__/FillsSub';
@@ -64,7 +69,6 @@ export const FILLS_QUERY = gql`
party(id: $partyId) {
id
tradesConnection(marketId: $marketId, pagination: $pagination) {
totalCount
edges {
node {
...FillFields
@@ -74,6 +78,8 @@ export const FILLS_QUERY = gql`
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
@@ -90,16 +96,24 @@ export const FILLS_SUB = gql`
`;
const update = (
data: Fills_party_tradesConnection_edges[],
data: (Fills_party_tradesConnection_edges | null)[],
delta: FillFields[]
) => {
return produce(data, (draft) => {
delta.forEach((node) => {
const index = draft.findIndex((edge) => edge.node.id === node.id);
orderBy(delta, 'createdAt').forEach((node) => {
const index = draft.findIndex((edge) => edge?.node.id === node.id);
if (index !== -1) {
Object.assign(draft[index].node, node);
if (draft[index]?.node) {
Object.assign(
draft[index]?.node as Fills_party_tradesConnection_edges_node,
node
);
}
} else {
draft.unshift({ node, cursor: '', __typename: 'TradeEdge' });
const firstNode = draft[0]?.node;
if (firstNode && node.createdAt >= firstNode.createdAt) {
draft.unshift({ node, cursor: '', __typename: 'TradeEdge' });
}
}
});
});
@@ -113,40 +127,8 @@ const getData = (
const getPageInfo = (responseData: Fills): PageInfo | null =>
responseData.party?.tradesConnection.pageInfo || null;
const getTotalCount = (responseData: Fills): number | undefined =>
responseData.party?.tradesConnection.totalCount;
const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || [];
const append = (
data: Fills_party_tradesConnection_edges[] | null,
pageInfo: PageInfo,
insertionData: Fills_party_tradesConnection_edges[] | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination
) => {
if (data && insertionData && insertionPageInfo) {
if (pagination?.after) {
if (data[data.length - 1].cursor === pagination.after) {
return {
data: [...data, ...insertionData],
pageInfo: { ...pageInfo, endCursor: insertionPageInfo.endCursor },
};
} else {
const cursors = data.map((item) => item.cursor);
const startIndex = cursors.lastIndexOf(pagination.after);
if (startIndex !== -1) {
return {
data: [...data.slice(0, startIndex), ...insertionData],
pageInfo: { ...pageInfo, endCursor: insertionPageInfo.endCursor },
};
}
}
}
}
return { data, pageInfo };
};
export const fillsDataProvider = makeDataProvider(
FILLS_QUERY,
FILLS_SUB,
@@ -155,7 +137,6 @@ export const fillsDataProvider = makeDataProvider(
getDelta,
{
getPageInfo,
getTotalCount,
append,
first: 100,
}
+66 -12
View File
@@ -3,7 +3,11 @@ import { useCallback, useRef, useMemo } from 'react';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { FillsTable } from './fills-table';
import type { IGetRowsParams } from 'ag-grid-community';
import type {
IGetRowsParams,
BodyScrollEvent,
BodyScrollEndEvent,
} from 'ag-grid-community';
import { fillsDataProvider as dataProvider } from './fills-data-provider';
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
@@ -15,14 +19,46 @@ interface FillsManagerProps {
export const FillsManager = ({ partyId }: FillsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const dataRef = useRef<Fills_party_tradesConnection_edges[] | null>(null);
const dataRef = useRef<(Fills_party_tradesConnection_edges | null)[] | null>(
null
);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const scrolledToTop = useRef(true);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
if (!gridRef.current?.api) {
return;
}
gridRef.current.api.refreshInfiniteCache();
}, []);
const update = useCallback(
({ data }: { data: Fills_party_tradesConnection_edges[] }) => {
({
data,
delta,
}: {
data: (Fills_party_tradesConnection_edges | null)[];
delta: FillsSub_trades[];
}) => {
if (!gridRef.current?.api) {
return false;
}
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += delta.filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
@@ -35,7 +71,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
data,
totalCount,
}: {
data: Fills_party_tradesConnection_edges[];
data: (Fills_party_tradesConnection_edges | null)[];
totalCount?: number;
}) => {
dataRef.current = data;
@@ -48,7 +84,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
const variables = useMemo(() => ({ partyId }), [partyId]);
const { data, error, loading, load, totalCount } = useDataProvider<
Fills_party_tradesConnection_edges[],
(Fills_party_tradesConnection_edges | null)[],
FillsSub_trades[]
>({ dataProvider, update, insert, variables });
totalCountRef.current = totalCount;
@@ -60,15 +96,14 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
startRow,
endRow,
}: IGetRowsParams) => {
startRow += newRows.current;
endRow += newRows.current;
try {
if (dataRef.current && dataRef.current.length < endRow) {
await load({
first: endRow - startRow,
after: dataRef.current[dataRef.current.length - 1].cursor,
});
if (dataRef.current && dataRef.current.indexOf(null) < endRow) {
await load();
}
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow).map((edge) => edge.node)
? dataRef.current.slice(startRow, endRow).map((edge) => edge?.node)
: [];
let lastRow = -1;
if (totalCountRef.current !== undefined) {
@@ -77,6 +112,8 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
} else if (totalCountRef.current <= endRow) {
lastRow = totalCountRef.current;
}
} else if (rowsThisBlock.length < endRow - startRow) {
lastRow = rowsThisBlock.length;
}
successCallback(rowsThisBlock, lastRow);
} catch (e) {
@@ -84,9 +121,26 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
}
};
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) {
addNewRows();
}
};
const onBodyScroll = (event: BodyScrollEvent) => {
scrolledToTop.current = event.top <= 0;
};
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<FillsTable ref={gridRef} partyId={partyId} datasource={{ getRows }} />
<FillsTable
ref={gridRef}
partyId={partyId}
datasource={{ getRows }}
rowModelType="infinite"
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
/>
</AsyncRenderer>
);
};
+6 -29
View File
@@ -4,7 +4,7 @@ import { Side } from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import { FillsTable } from './fills-table';
import { generateFill, makeGetRows } from './test-helpers';
import { generateFill } from './test-helpers';
import type { FillFields } from './__generated__/FillFields';
const waitForGridToBeInTheDOM = () => {
@@ -47,12 +47,7 @@ describe('FillsTable', () => {
});
it('correct columns are rendered', async () => {
render(
<FillsTable
partyId="party-id"
datasource={{ getRows: makeGetRows([generateFill()]) }}
/>
);
render(<FillsTable partyId="party-id" rowData={[generateFill()]} />);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
@@ -84,12 +79,7 @@ describe('FillsTable', () => {
},
});
render(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([buyerFill]) }}
/>
);
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
@@ -126,12 +116,7 @@ describe('FillsTable', () => {
},
});
render(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([buyerFill]) }}
/>
);
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
@@ -163,10 +148,7 @@ describe('FillsTable', () => {
});
const { rerender } = render(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([takerFill]) }}
/>
<FillsTable partyId={partyId} rowData={[takerFill]} />
);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
@@ -184,12 +166,7 @@ describe('FillsTable', () => {
aggressor: Side.Buy,
});
rerender(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([makerFill]) }}
/>
);
rerender(<FillsTable partyId={partyId} rowData={[makerFill]} />);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
+384 -9
View File
@@ -1,22 +1,397 @@
import type { Story, Meta } from '@storybook/react';
import type { FillsTableProps } from './fills-table';
import type { Props } from './fills-table';
import type { AgGridReact } from 'ag-grid-react';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useCallback, useRef } from 'react';
import { FillsTable } from './fills-table';
import { generateFills, makeGetRows } from './test-helpers';
import { generateFills, generateFill } from './test-helpers';
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
import type { FillsSub_trades } from './__generated__/FillsSub';
import type {
IGetRowsParams,
BodyScrollEvent,
BodyScrollEndEvent,
} from 'ag-grid-community';
export default {
component: FillsTable,
title: 'FillsTable',
} as Meta;
const Template: Story<FillsTableProps> = (args) => <FillsTable {...args} />;
const Template: Story<Props> = (args) => <FillsTable {...args} />;
export const Default = Template.bind({});
const createdAt = new Date('2005-04-02 21:37:00').getTime();
const fills = generateFills();
Default.args = {
partyId: 'party-id',
datasource: {
getRows: makeGetRows(
fills.party?.tradesConnection.edges.map((e) => e.node) || []
),
},
rowData: fills.party?.tradesConnection.edges.map((e) => e.node) || [],
};
const getData = (
start: number,
end: number
): Fills_party_tradesConnection_edges[] =>
new Array(end - start).fill(null).map((v, i) => ({
__typename: 'TradeEdge',
node: generateFill({
id: (start + i).toString(),
createdAt: new Date(createdAt - 1000 * (start + i)).toISOString(),
}),
cursor: (start + i).toString(),
}));
const totalCount = 550;
const partyId = 'partyId';
const useDataProvider = ({
insert,
}: {
insert: ({
insertionData,
data,
totalCount,
}: {
insertionData: Fills_party_tradesConnection_edges[];
data: Fills_party_tradesConnection_edges[];
totalCount?: number;
}) => boolean;
}) => {
const data = [...getData(0, 100), ...new Array(totalCount - 100).fill(null)];
return {
data,
error: null,
loading: false,
load: (start?: number, end?: number) => {
if (start === undefined) {
start = data.findIndex((v) => !v);
}
if (end === undefined) {
end = start + 100;
}
end = Math.min(end, totalCount);
const insertionData = getData(start, end);
data.splice(start, end - start, ...insertionData);
insert({ data, totalCount, insertionData });
return Promise.resolve();
},
totalCount,
};
};
interface PaginationManagerProps {
pagination: boolean;
}
const PaginationManager = ({ pagination }: PaginationManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const dataRef = useRef<Fills_party_tradesConnection_edges[] | null>(null);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const scrolledToTop = useRef(true);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
if (!gridRef.current?.api) {
return;
}
gridRef.current.api.refreshInfiniteCache();
}, []);
const update = useCallback(
({
data,
delta,
}: {
data: Fills_party_tradesConnection_edges[];
delta: FillsSub_trades[];
}) => {
if (!gridRef.current?.api) {
return false;
}
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0].node.createdAt;
if (createdAt) {
newRows.current += delta.filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
},
[]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: Fills_party_tradesConnection_edges[];
totalCount?: number;
}) => {
dataRef.current = data;
totalCountRef.current = totalCount;
return true;
},
[]
);
const { data, error, loading, load, totalCount } = useDataProvider({
insert,
});
totalCountRef.current = totalCount;
dataRef.current = data;
const getRows = async ({
successCallback,
failCallback,
startRow,
endRow,
}: IGetRowsParams) => {
startRow += newRows.current;
endRow += newRows.current;
try {
if (
dataRef.current &&
dataRef.current.slice(startRow, endRow).some((i) => !i)
) {
await load(startRow, endRow);
}
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow).map((edge) => edge.node)
: [];
let lastRow = -1;
if (totalCountRef.current !== undefined) {
if (!totalCountRef.current) {
lastRow = 0;
} else {
lastRow = totalCountRef.current;
}
} else if (rowsThisBlock.length < endRow - startRow) {
lastRow = rowsThisBlock.length;
}
successCallback(rowsThisBlock, lastRow);
} catch (e) {
failCallback();
}
};
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) {
addNewRows();
}
};
const onBodyScroll = (event: BodyScrollEvent) => {
scrolledToTop.current = event.top <= 0;
};
// id and onclick is needed only for mocked data
let id = 0;
const onClick = () => {
if (!dataRef.current) {
return;
}
const node = generateFill({
id: (--id).toString(),
createdAt: new Date(createdAt - 1000 * id).toISOString(),
});
update({
data: [
{ cursor: '0', node, __typename: 'TradeEdge' },
...dataRef.current,
],
delta: [node],
});
};
return (
<>
<Button onClick={onClick}>Add row on top</Button>
<AsyncRenderer loading={loading} error={error} data={data}>
<FillsTable
rowModelType="infinite"
pagination={pagination}
ref={gridRef}
partyId={partyId}
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
/>
</AsyncRenderer>
</>
);
};
const PaginationTemplate: Story<PaginationManagerProps> = (args) => (
<PaginationManager {...args} />
);
export const Pagination = PaginationTemplate.bind({});
Pagination.args = { pagination: true };
export const PaginationScroll = PaginationTemplate.bind({});
PaginationScroll.args = { pagination: false };
const InfiniteScrollManager = () => {
const gridRef = useRef<AgGridReact | null>(null);
const dataRef = useRef<(Fills_party_tradesConnection_edges | null)[] | null>(
null
);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const scrolledToTop = useRef(true);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
if (!gridRef.current?.api) {
return;
}
gridRef.current.api.refreshInfiniteCache();
}, []);
const update = useCallback(
({
data,
delta,
}: {
data: (Fills_party_tradesConnection_edges | null)[];
delta: FillsSub_trades[];
}) => {
if (!gridRef.current?.api) {
return false;
}
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += delta.filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
},
[]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: Fills_party_tradesConnection_edges[];
totalCount?: number;
}) => {
dataRef.current = data;
totalCountRef.current = totalCount;
return true;
},
[]
);
const { data, error, loading, load, totalCount } = useDataProvider({
insert,
});
totalCountRef.current = totalCount;
dataRef.current = data;
const getRows = async ({
successCallback,
failCallback,
startRow,
endRow,
}: IGetRowsParams) => {
startRow += newRows.current;
endRow += newRows.current;
try {
if (dataRef.current && dataRef.current.indexOf(null) < endRow) {
await load();
}
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow).map((edge) => edge?.node)
: [];
let lastRow = -1;
if (totalCountRef.current !== undefined) {
if (!totalCountRef.current) {
lastRow = 0;
} else if (totalCountRef.current <= endRow) {
lastRow = totalCountRef.current;
}
} else if (rowsThisBlock.length < endRow - startRow) {
lastRow = rowsThisBlock.length;
}
successCallback(rowsThisBlock, lastRow);
} catch (e) {
failCallback();
}
};
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) {
addNewRows();
}
};
const onBodyScroll = (event: BodyScrollEvent) => {
scrolledToTop.current = event.top <= 0;
};
// id and onclick is needed only for mocked data
let id = 0;
const onClick = () => {
if (!dataRef.current) {
return;
}
const node = generateFill({
id: (--id).toString(),
createdAt: new Date(createdAt - 1000 * id).toISOString(),
});
update({
data: [
{ cursor: '0', node, __typename: 'TradeEdge' },
...dataRef.current,
],
delta: [node],
});
};
return (
<>
<Button onClick={onClick}>Add row on top</Button>
<AsyncRenderer loading={loading} error={error} data={data}>
<FillsTable
ref={gridRef}
partyId={partyId}
datasource={{ getRows }}
rowModelType="infinite"
onBodyScroll={onBodyScroll}
onBodyScrollEnd={onBodyScrollEnd}
/>
</AsyncRenderer>
</>
);
};
const InfiniteScrollTemplate: Story<Record<string, never>> = () => (
<InfiniteScrollManager />
);
export const InfiniteScroll = InfiniteScrollTemplate.bind({});
+61 -22
View File
@@ -6,21 +6,32 @@ import {
getDateTimeFormat,
t,
} from '@vegaprotocol/react-helpers';
import { Side } from '@vegaprotocol/types';
import { AgGridColumn } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { forwardRef } from 'react';
import type { FillFields } from './__generated__/FillFields';
import type { ValueFormatterParams, IDatasource } from 'ag-grid-community';
import type { ValueFormatterParams } from 'ag-grid-community';
import BigNumber from 'bignumber.js';
import { Side } from '@vegaprotocol/types';
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
import type {
FillFields,
FillFields_market_tradableInstrument_instrument_product,
} from './__generated__/FillFields';
import type { Fills_party_tradesConnection_edges_node } from './__generated__/Fills';
export interface FillsTableProps {
export type Props = (AgGridReactProps | AgReactUiProps) & {
partyId: string;
datasource: IDatasource;
}
};
export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
({ partyId, datasource }, ref) => {
type AccountsTableValueFormatterParams = Omit<
ValueFormatterParams,
'data' | 'value'
> & {
data: Fills_party_tradesConnection_edges_node | null;
};
export const FillsTable = forwardRef<AgGridReact, Props>(
({ partyId, ...props }, ref) => {
return (
<AgGrid
ref={ref}
@@ -28,8 +39,7 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data?.id}
rowModelType="infinite"
datasource={datasource}
{...props}
>
<AgGridColumn headerName={t('Market')} field="market.name" />
<AgGridColumn
@@ -69,7 +79,11 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
<AgGridColumn
headerName={t('Date')}
field="createdAt"
valueFormatter={({ value }: ValueFormatterParams) => {
valueFormatter={({
value,
}: AccountsTableValueFormatterParams & {
value: Fills_party_tradesConnection_edges_node['createdAt'];
}) => {
if (value === undefined) {
return value;
}
@@ -81,9 +95,14 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
}
);
const formatPrice = ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
const formatPrice = ({
value,
data,
}: AccountsTableValueFormatterParams & {
value?: Fills_party_tradesConnection_edges_node['price'];
}) => {
if (value === undefined || !data) {
return undefined;
}
const asset =
data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
@@ -95,9 +114,14 @@ const formatPrice = ({ value, data }: ValueFormatterParams) => {
};
const formatSize = (partyId: string) => {
return ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
return ({
value,
data,
}: AccountsTableValueFormatterParams & {
value?: Fills_party_tradesConnection_edges_node['size'];
}) => {
if (value === undefined || !data) {
return undefined;
}
let prefix;
if (data?.buyer.id === partyId) {
@@ -114,9 +138,14 @@ const formatSize = (partyId: string) => {
};
};
const formatTotal = ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
const formatTotal = ({
value,
data,
}: AccountsTableValueFormatterParams & {
value?: Fills_party_tradesConnection_edges_node['price'];
}) => {
if (value === undefined || !data) {
return undefined;
}
const asset =
data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
@@ -131,7 +160,12 @@ const formatTotal = ({ value, data }: ValueFormatterParams) => {
};
const formatRole = (partyId: string) => {
return ({ value, data }: ValueFormatterParams) => {
return ({
value,
data,
}: AccountsTableValueFormatterParams & {
value?: Fills_party_tradesConnection_edges_node['aggressor'];
}) => {
if (value === undefined) {
return value;
}
@@ -156,7 +190,12 @@ const formatRole = (partyId: string) => {
};
const formatFee = (partyId: string) => {
return ({ value, data }: ValueFormatterParams) => {
return ({
value,
data,
}: AccountsTableValueFormatterParams & {
value?: FillFields_market_tradableInstrument_instrument_product;
}) => {
if (value === undefined) {
return value;
}
+3 -9
View File
@@ -1,5 +1,4 @@
import merge from 'lodash/merge';
import type { IGetRowsParams } from 'ag-grid-community';
import type { PartialDeep } from 'type-fest';
import type {
Fills,
@@ -52,7 +51,6 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
id: 'buyer-id',
tradesConnection: {
__typename: 'TradeConnection',
totalCount: 1,
edges: fills.map((f) => {
return {
__typename: 'TradeEdge',
@@ -64,6 +62,8 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
__typename: 'PageInfo',
startCursor: '1',
endCursor: '2',
hasNextPage: false,
hasPreviousPage: false,
},
},
__typename: 'Party',
@@ -79,7 +79,7 @@ export const generateFill = (
const defaultFill: Fills_party_tradesConnection_edges_node = {
__typename: 'Trade',
id: '0',
createdAt: new Date().toISOString(),
createdAt: '2005-04-02T19:37:00.000Z',
price: '10000000',
size: '50000',
buyOrder: 'buy-order',
@@ -133,9 +133,3 @@ export const generateFill = (
return merge(defaultFill, override);
};
export const makeGetRows =
(data: Fills_party_tradesConnection_edges_node[]) =>
({ successCallback }: IGetRowsParams) => {
successCallback(data, data.length);
};
@@ -1,46 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
// ====================================================
// GraphQL fragment: MarketDataFields
// ====================================================
export interface MarketDataFields_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Current state of the market
*/
state: MarketState;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
}
export interface MarketDataFields {
__typename: "MarketData";
/**
* market id of the associated mark price
*/
market: MarketDataFields_market;
/**
* the highest price level on an order book for buy orders.
*/
bestBidPrice: string;
/**
* the lowest price level on an order book for offer orders.
*/
bestOfferPrice: string;
/**
* the mark price (actually an unsigned int)
*/
markPrice: string;
}
@@ -1,53 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
// ====================================================
// GraphQL subscription operation: MarketDataSub
// ====================================================
export interface MarketDataSub_marketData_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Current state of the market
*/
state: MarketState;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
}
export interface MarketDataSub_marketData {
__typename: "MarketData";
/**
* market id of the associated mark price
*/
market: MarketDataSub_marketData_market;
/**
* the highest price level on an order book for buy orders.
*/
bestBidPrice: string;
/**
* the lowest price level on an order book for offer orders.
*/
bestOfferPrice: string;
/**
* the mark price (actually an unsigned int)
*/
markPrice: string;
}
export interface MarketDataSub {
/**
* Subscribe to the mark price changes
*/
marketData: MarketDataSub_marketData;
}
@@ -1,126 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: Markets
// ====================================================
export interface Markets_markets_data_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Current state of the market
*/
state: MarketState;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
}
export interface Markets_markets_data {
__typename: "MarketData";
/**
* market id of the associated mark price
*/
market: Markets_markets_data_market;
/**
* the highest price level on an order book for buy orders.
*/
bestBidPrice: string;
/**
* the lowest price level on an order book for offer orders.
*/
bestOfferPrice: string;
/**
* the mark price (actually an unsigned int)
*/
markPrice: string;
}
export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset {
__typename: "Asset";
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
}
export interface Markets_markets_tradableInstrument_instrument_product {
__typename: "Future";
/**
* The name of the asset (string)
*/
settlementAsset: Markets_markets_tradableInstrument_instrument_product_settlementAsset;
}
export interface Markets_markets_tradableInstrument_instrument {
__typename: "Instrument";
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
/**
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
*/
product: Markets_markets_tradableInstrument_instrument_product;
}
export interface Markets_markets_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: Markets_markets_tradableInstrument_instrument;
}
export interface Markets_markets {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
name: string;
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* marketData for the given market
*/
data: Markets_markets_data | null;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: Markets_markets_tradableInstrument;
}
export interface Markets {
/**
* One or more instruments that are trading on the VEGA network
*/
markets: Markets_markets[] | null;
}
@@ -1,3 +1,2 @@
export * from './__generated__';
export * from './landing';
export * from './markets-container';
@@ -3,7 +3,7 @@
// @generated
// This file was automatically generated and should not be edited.
import { Interval, MarketState } from "@vegaprotocol/types";
import { Interval, MarketState, MarketTradingMode } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: MarketList
@@ -112,6 +112,10 @@ export interface MarketList_markets {
* Current state of the market
*/
state: MarketState;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
/**
* marketData for the given market
*/
@@ -1,3 +1,4 @@
export * from './MarketDataFields';
export * from './MarketDataSub';
export * from './MarketList';
export * from './Markets';
@@ -2,4 +2,4 @@ export * from './market-list-table';
export * from './markets-container';
export * from './markets-data-provider';
export * from './summary-cell';
export * from './__generated__/MarketList';
export * from './__generated__';
@@ -1,5 +1,5 @@
import { forwardRef } from 'react';
import type { IDatasource, ValueFormatterParams } from 'ag-grid-community';
import type { ValueFormatterParams } from 'ag-grid-community';
import {
PriceFlashCell,
addDecimalsFormatNumber,
@@ -8,95 +8,119 @@ import {
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Markets_markets } from '../__generated__/Markets';
import type {
AgGridReact,
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react';
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
import type {
Markets_markets,
Markets_markets_data,
} from './__generated__/Markets';
interface MarketListTableProps {
datasource: IDatasource;
onRowClicked: (marketId: string) => void;
}
type Props = AgGridReactProps | AgReactUiProps;
export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
({ datasource, onRowClicked }, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No markets')}
rowModelType="infinite"
datasource={datasource}
getRowId={({ data }) => data?.id}
ref={ref}
defaultColDef={{
flex: 1,
resizable: true,
type MarketListTableValueFormatterParams = Omit<
ValueFormatterParams,
'data' | 'value'
> & {
data: Markets_markets;
};
export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No markets')}
getRowId={({ data }) => data?.id}
ref={ref}
defaultColDef={{
flex: 1,
resizable: true,
}}
suppressCellFocus={true}
components={{ PriceFlashCell }}
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset.symbol"
/>
<AgGridColumn
headerName={t('Trading mode')}
field="data"
minWidth={200}
valueFormatter={({
value,
}: MarketListTableValueFormatterParams & {
value?: Markets_markets_data;
}) => {
if (!value) return value;
const { market, trigger } = value;
return market &&
market.tradingMode === MarketTradingMode.MonitoringAuction &&
trigger &&
trigger !== AuctionTrigger.Unspecified
? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}`
: formatLabel(market?.tradingMode);
}}
suppressCellFocus={true}
onRowClicked={({ data }: { data: Markets_markets }) =>
onRowClicked(data.id)
/>
<AgGridColumn
headerName={t('Best bid')}
field="data.bestBidPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({
value,
data,
}: MarketListTableValueFormatterParams & {
value?: Markets_markets_data['bestBidPrice'];
}) =>
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
components={{ PriceFlashCell }}
>
<AgGridColumn
headerName={t('Market')}
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset.symbol"
/>
<AgGridColumn
headerName={t('Trading mode')}
field="data"
minWidth={200}
valueFormatter={({ value }: ValueFormatterParams) => {
if (!value) return value;
const { market, trigger } = value;
return market &&
market.tradingMode === MarketTradingMode.MonitoringAuction &&
trigger &&
trigger !== AuctionTrigger.Unspecified
? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}`
: formatLabel(market?.tradingMode);
}}
/>
<AgGridColumn
headerName={t('Best bid')}
field="data.bestBidPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn
headerName={t('Best offer')}
field="data.bestOfferPrice"
type="rightAligned"
valueFormatter={({ value, data }: ValueFormatterParams) =>
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
cellRenderer="PriceFlashCell"
/>
<AgGridColumn
headerName={t('Mark price')}
field="data.markPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn headerName={t('Description')} field="name" />
</AgGrid>
);
}
);
/>
<AgGridColumn
headerName={t('Best offer')}
field="data.bestOfferPrice"
type="rightAligned"
valueFormatter={({
value,
data,
}: MarketListTableValueFormatterParams & {
value?: Markets_markets_data['bestOfferPrice'];
}) =>
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
cellRenderer="PriceFlashCell"
/>
<AgGridColumn
headerName={t('Mark price')}
field="data.markPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({
value,
data,
}: MarketListTableValueFormatterParams & {
value?: Markets_markets_data['markPrice'];
}) =>
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn headerName={t('Description')} field="name" />
</AgGrid>
);
});
export default MarketListTable;
@@ -8,7 +8,7 @@ import type { IGetRowsParams } from 'ag-grid-community';
import type {
Markets_markets,
Markets_markets_data,
} from '../../components/__generated__/Markets';
} from './__generated__/Markets';
import { marketsDataProvider as dataProvider } from './markets-data-provider';
import { MarketState } from '@vegaprotocol/types';
@@ -42,13 +42,15 @@ export const MarketsContainer = () => {
const lastRow = dataRef.current?.length ?? -1;
successCallback(rowsThisBlock, lastRow);
};
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<MarketListTable
rowModelType="infinite"
datasource={{ getRows }}
ref={gridRef}
onRowClicked={(id) => push(`/markets/${id}`)}
onRowClicked={({ data }: { data: Markets_markets }) =>
push(`/markets/${data.id}`)
}
/>
</AsyncRenderer>
);
@@ -3,13 +3,10 @@ import { gql } from '@apollo/client';
import type {
Markets,
Markets_markets,
} from '../../components/__generated__/Markets';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type {
MarketDataSub,
MarketDataSub_marketData,
} from '../../components/__generated__/MarketDataSub';
} from './';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
const MARKET_DATA_FRAGMENT = gql`
fragment MarketDataFields on MarketData {
@@ -57,6 +54,7 @@ export const MARKET_LIST_QUERY = gql`
id
decimalPlaces
state
tradingMode
data {
market {
id
@@ -1,4 +1,4 @@
import { MarketState } from '@vegaprotocol/types';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import orderBy from 'lodash/orderBy';
import type {
MarketList,
@@ -13,7 +13,11 @@ export const lastPrice = ({ candles }: MarketList_markets) =>
export const mapDataToMarketList = ({ markets }: MarketList) =>
orderBy(
markets
?.filter((m) => m.state !== MarketState.Rejected)
?.filter(
(m) =>
m.state !== MarketState.Rejected &&
m.tradingMode !== MarketTradingMode.NoTrading
)
.map((m) => {
return {
id: m.id,
@@ -0,0 +1,102 @@
import {
addDecimal,
t,
addDecimalsFormatNumber,
} from '@vegaprotocol/react-helpers';
import { OrderType } from '@vegaprotocol/types';
import { FormGroup, Input, InputError, Button } from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import Icon from 'react-syntax-highlighter';
import { OrderDialogWrapper } from '@vegaprotocol/wallet';
import type { Order } from '@vegaprotocol/wallet';
interface OrderEditDialogProps {
title: string;
order: Order | null;
edit: (body: Order) => Promise<unknown>;
}
interface FormFields {
entryPrice: string;
}
export const OrderEditDialog = ({
order,
title,
edit,
}: OrderEditDialogProps) => {
const headerClassName = 'text-h5 font-bold text-black dark:text-white';
const {
register,
formState: { errors },
handleSubmit,
} = useForm<FormFields>({
defaultValues: {
entryPrice: order?.price
? addDecimal(order?.price, order?.market?.decimalPlaces ?? 0)
: '',
},
});
if (!order) return null;
return (
<OrderDialogWrapper title={title} icon={<Icon name="hand-up" size={20} />}>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{order.market && (
<div>
<p className={headerClassName}>{t(`Market`)}</p>
<p>{t(`${order.market.name}`)}</p>
</div>
)}
{order.type === OrderType.Limit && order.market && (
<div>
<p className={headerClassName}>{t(`Last price`)}</p>
<p>
{addDecimalsFormatNumber(order.price, order.market.decimalPlaces)}
</p>
</div>
)}
<div>
<p className={headerClassName}>{t(`Amount remaining`)}</p>
<p
className={
order.side === 'Buy'
? 'text-dark-green dark:text-vega-green'
: 'text-red dark:text-vega-red'
}
>
{order.side === 'Buy' ? '+' : '-'}
{order.size}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-12">
<form
onSubmit={handleSubmit(async (data) => {
await edit({
...order,
price: data.entryPrice,
});
})}
data-testid="edit-order"
>
<FormGroup label={t('Entry price')} labelFor="entryPrice">
<Input
{...register('entryPrice', { required: t('Required') })}
id="entryPrice"
type="text"
/>
{errors.entryPrice?.message && (
<InputError intent="danger" className="mt-4">
{errors.entryPrice.message}
</InputError>
)}
</FormGroup>
<Button variant="primary" type="submit">
{t('Update')}
</Button>
</form>
</div>
</OrderDialogWrapper>
);
};
@@ -1,5 +1,9 @@
import { act, render, screen } from '@testing-library/react';
import { addDecimal, getDateTimeFormat } from '@vegaprotocol/react-helpers';
import {
addDecimal,
formatLabel,
getDateTimeFormat,
} from '@vegaprotocol/react-helpers';
import type { Orders_party_orders } from '../__generated__/Orders';
import { OrderStatus, OrderRejectionReason } from '@vegaprotocol/types';
import { OrderListTable } from './order-list';
@@ -16,7 +20,12 @@ const generateJsx = (
return (
<MockedProvider>
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<OrderListTable data={orders} cancel={jest.fn()} />
<OrderListTable
data={orders}
cancel={jest.fn()}
setEditOrderDialogOpen={jest.fn()}
setEditOrder={jest.fn()}
/>
</VegaWalletContext.Provider>
</MockedProvider>
);
@@ -36,7 +45,7 @@ describe('OrderListTable', () => {
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(10);
expect(headers).toHaveLength(11);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Market',
'Amount',
@@ -47,6 +56,7 @@ describe('OrderListTable', () => {
'Time In Force',
'Created At',
'Updated At',
'Edit',
'Cancel',
]);
});
@@ -67,6 +77,7 @@ describe('OrderListTable', () => {
marketOrder.timeInForce,
getDateTimeFormat().format(new Date(marketOrder.createdAt)),
'-',
'Edit',
'Cancel',
];
cells.forEach((cell, i) =>
@@ -92,6 +103,7 @@ describe('OrderListTable', () => {
)}`,
getDateTimeFormat().format(new Date(limitOrder.createdAt)),
'-',
'Edit',
'Cancel',
];
cells.forEach((cell, i) =>
@@ -110,7 +122,7 @@ describe('OrderListTable', () => {
});
const cells = screen.getAllByRole('gridcell');
expect(cells[3]).toHaveTextContent(
`${rejectedOrder.status}: ${rejectedOrder.rejectionReason}`
`${rejectedOrder.status}: ${formatLabel(rejectedOrder.rejectionReason)}`
);
});
});
@@ -1,5 +1,5 @@
import type { Story, Meta } from '@storybook/react';
import { OrderType, OrderStatus } from '@vegaprotocol/types';
import { OrderType, OrderStatus, OrderTimeInForce } from '@vegaprotocol/types';
import { OrderList, OrderListTable } from './order-list';
import { useState } from 'react';
import type { Order, VegaTxState } from '@vegaprotocol/wallet';
@@ -15,7 +15,16 @@ const Template: Story = (args) => {
const cancel = () => Promise.resolve();
return (
<div style={{ height: 1000 }}>
<OrderListTable data={args.data} cancel={cancel} />
<OrderListTable
data={args.data}
cancel={cancel}
setEditOrderDialogOpen={() => {
return;
}}
setEditOrder={() => {
return;
}}
/>
</div>
);
};
@@ -39,12 +48,22 @@ const Template2: Story = (args) => {
price: '1000',
market: { name: 'ETH/DAI (30 Jun 2022)', decimalPlaces: 5 },
type: OrderType.Limit,
timeInForce: OrderTimeInForce.GTC,
};
const reset = () => null;
return (
<>
<div style={{ height: 1000 }}>
<OrderListTable data={args.data} cancel={cancel} />
<OrderListTable
data={args.data}
cancel={cancel}
setEditOrderDialogOpen={() => {
return;
}}
setEditOrder={() => {
return;
}}
/>
</div>
<VegaTransactionDialog
orderDialogOpen={open}
@@ -1,6 +1,11 @@
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
import type { Orders_party_orders } from '../__generated__/Orders';
import { addDecimal, getDateTimeFormat, t } from '@vegaprotocol/react-helpers';
import {
addDecimal,
formatLabel,
getDateTimeFormat,
t,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit';
import type {
ICellRendererParams,
@@ -12,6 +17,8 @@ import { forwardRef, useState } from 'react';
import BigNumber from 'bignumber.js';
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
import { VegaTransactionDialog } from '@vegaprotocol/wallet';
import { useOrderEdit } from '../../order-hooks/use-order-edit';
import { OrderEditDialog } from './order-edit-dialog';
interface OrderListProps {
data: Orders_party_orders[] | null;
@@ -21,11 +28,22 @@ interface OrderListProps {
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
({ data, showCancelled = true }, ref) => {
const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false);
const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false);
const [editOrder, setEditOrder] = useState<Orders_party_orders | null>(
null
);
const { transaction, updatedOrder, reset, cancel } = useOrderCancel();
const {
transaction: editTransaction,
updatedOrder: editedOrder,
reset: resetEdit,
edit,
} = useOrderEdit();
const ordersData = showCancelled
? data
: data?.filter((o) => o.status !== OrderStatus.Cancelled) || null;
const getDialogTitle = (status?: string) => {
const getCancelDialogTitle = (status?: string) => {
switch (status) {
case OrderStatus.Cancelled:
return 'Order cancelled';
@@ -37,18 +55,51 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
return 'Cancellation failed';
}
};
const getEditDialogTitle = () =>
editedOrder
? t(
`Order ${
editOrder?.market?.tradableInstrument.instrument.code ?? ''
} updated`
)
: t(
`Edit ${
editOrder?.market?.tradableInstrument.instrument.code ?? ''
} order`
);
return (
<>
<OrderListTable data={ordersData} cancel={cancel} ref={ref} />
<OrderListTable
data={ordersData}
cancel={cancel}
ref={ref}
setEditOrderDialogOpen={setEditOrderDialogOpen}
setEditOrder={setEditOrder}
/>
<VegaTransactionDialog
key={`cancel-order-dialog-${transaction.txHash}`}
orderDialogOpen={cancelOrderDialogOpen}
setOrderDialogOpen={setCancelOrderDialogOpen}
finalizedOrder={updatedOrder}
transaction={transaction}
reset={reset}
title={getDialogTitle(updatedOrder?.status)}
title={getCancelDialogTitle(updatedOrder?.status)}
finalizedOrder={updatedOrder}
/>
<VegaTransactionDialog
key={`edit-order-dialog-${transaction.txHash}`}
orderDialogOpen={editOrderDialogOpen}
setOrderDialogOpen={setEditOrderDialogOpen}
transaction={editTransaction}
reset={resetEdit}
title={getEditDialogTitle()}
finalizedOrder={editedOrder}
>
<OrderEditDialog
title={getEditDialogTitle()}
order={editOrder}
edit={edit}
/>
</VegaTransactionDialog>
</>
);
}
@@ -57,10 +108,12 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
interface OrderListTableProps {
data: Orders_party_orders[] | null;
cancel: (body?: unknown) => Promise<unknown>;
setEditOrderDialogOpen: (value: boolean) => void;
setEditOrder: (order: Orders_party_orders | null) => void;
}
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ data, cancel }, ref) => {
({ data, cancel, setEditOrderDialogOpen, setEditOrder }, ref) => {
return (
<AgGrid
ref={ref}
@@ -91,7 +144,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
field="status"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (value === OrderStatus.Rejected) {
return `${value}: ${data.rejectionReason}`;
return `${value}: ${formatLabel(data.rejectionReason)}`;
}
return value;
@@ -147,6 +200,34 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
field="edit"
cellRenderer={({ data }: ICellRendererParams) => {
if (
![
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(data.status)
) {
return (
<Button
data-testid="edit"
variant="secondary"
onClick={() => {
setEditOrderDialogOpen(true);
setEditOrder(data);
}}
>
Edit
</Button>
);
}
return null;
}}
/>
<AgGridColumn
field="cancel"
cellRenderer={({ data }: ICellRendererParams) => {
@@ -15,6 +15,10 @@ export interface OrderEvent_busEvents_event_TimeUpdate {
export interface OrderEvent_busEvents_event_Order_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
@@ -14,8 +14,10 @@ export const ORDER_EVENT_SUB = gql`
size
price
timeInForce
expiresAt
side
market {
id
name
decimalPlaces
}
@@ -67,7 +67,7 @@ export const useOrderCancel = () => {
if (res?.signature) {
const resId = order.id ?? determineId(res.signature);
setUpdatedOrder(null);
// setId(resId);
if (resId) {
// Start a subscription looking for the newly created order
subRef.current = client
@@ -0,0 +1,221 @@
import { act, renderHook } from '@testing-library/react-hooks';
import type {
VegaKeyExtended,
VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import {
VegaWalletOrderSide,
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { useOrderEdit } from './use-order-edit';
import type {
OrderEvent,
OrderEvent_busEvents,
} from './__generated__/OrderEvent';
import { ORDER_EVENT_SUB } from './order-event-query';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import {
MarketTradingMode,
MarketState,
OrderTimeInForce,
} from '@vegaprotocol/types';
import type {
OrderAmendmentBodyOrderAmendment,
OrderAmendmentBody,
} from '@vegaprotocol/vegawallet-service-api-client';
const defaultWalletContext = {
keypair: null,
keypairs: [],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPublicKey: jest.fn(),
connector: null,
};
function setup(context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderEvent> = {
request: {
query: ORDER_EVENT_SUB,
variables: {
partyId: context?.keypair?.pub || '',
},
},
result: {
data: {
busEvents: [
{
type: 'Order',
event: {
type: 'Limit',
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: 'Active',
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: 'GTC',
side: 'Buy',
market: {
name: 'UNIDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
__typename: 'Market',
},
__typename: 'Order',
},
__typename: 'BusEvent',
} as OrderEvent_busEvents,
],
},
},
};
const filterMocks: MockedResponse<OrderEvent> = {
request: {
query: ORDER_EVENT_SUB,
variables: {
partyId: context?.keypair?.pub || '',
},
},
result: {
data: {
busEvents: [
{
type: 'Order',
event: {
type: 'Limit',
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: 'Active',
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: 'GTC',
side: 'Buy',
market: {
name: 'UNIDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
__typename: 'Market',
},
__typename: 'Order',
},
__typename: 'BusEvent',
} as OrderEvent_busEvents,
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderEdit(), { wrapper });
}
const defaultMarket = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
tradingMode: MarketTradingMode.Continuous,
state: MarketState.Active,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
product: {
__typename: 'Future',
quoteName: 'quote-name',
},
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: {
__typename: 'Trade',
price: '100',
},
},
};
const order = {
id: 'order-id',
type: VegaWalletOrderType.Limit,
size: '10',
timeInForce: OrderTimeInForce.GTT, // order timeInForce is transformed to wallet timeInForce
side: VegaWalletOrderSide.Buy,
price: '1234567.89',
expiration: new Date('2022-01-01'),
expiresAt: new Date('2022-01-01'),
status: VegaTxStatus.Pending,
rejectionReason: null,
market: {
id: 'market-id',
decimalPlaces: 2,
name: 'ETHDAI',
positionDecimalPlaces: 2,
},
};
describe('useOrderEdit', () => {
it('should edit a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const { result } = setup({
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
});
await act(async () => {
result.current.edit(order);
});
expect(mockSendTx).toHaveBeenCalledWith({
pubKey: keypair.pub,
propagate: true,
orderAmendment: {
orderId: 'order-id',
marketId: defaultMarket.id, // Market provided from hook argument
timeInForce: VegaWalletOrderTimeInForce.GTT,
price: { value: '123456789' }, // Decimal removed
sizeDelta: 0,
expiresAt: { value: order.expiration?.getTime() + '000000' }, // Nanoseconds append
} as unknown as OrderAmendmentBodyOrderAmendment,
} as OrderAmendmentBody);
});
it('has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.edit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', async () => {
const mockSendTx = jest.fn();
const { result } = setup({
sendTx: mockSendTx,
keypairs: [],
keypair: null,
});
await act(async () => {
result.current.edit(order);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,119 @@
import { useApolloClient } from '@apollo/client';
import { determineId, removeDecimal } from '@vegaprotocol/react-helpers';
import { useState, useCallback, useEffect, useRef } from 'react';
import type { Order } from '@vegaprotocol/wallet';
import { VegaWalletOrderTimeInForce } from '@vegaprotocol/wallet';
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
import { ORDER_EVENT_SUB } from './order-event-query';
import type { Subscription } from 'zen-observable-ts';
import type {
OrderEvent_busEvents_event_Order,
OrderEvent,
OrderEventVariables,
} from './__generated__';
import * as Sentry from '@sentry/react';
export const useOrderEdit = () => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [updatedOrder, setUpdatedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
const reset = useCallback(() => {
resetTransaction();
setUpdatedOrder(null);
subRef.current?.unsubscribe();
}, [resetTransaction]);
useEffect(() => {
return () => {
resetTransaction();
setUpdatedOrder(null);
subRef.current?.unsubscribe();
};
}, [resetTransaction]);
const edit = useCallback(
async (order: Order) => {
if (!keypair || !order.market || !order.market.id) {
return;
}
setUpdatedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderAmendment: {
orderId: order.id,
marketId: order.market.id,
price: {
value: removeDecimal(order.price, order.market?.decimalPlaces),
},
timeInForce: VegaWalletOrderTimeInForce[order.timeInForce],
sizeDelta: 0,
expiresAt: order.expiresAt
? {
value:
// Wallet expects timestamp in nanoseconds,
// we don't have that level of accuracy so just append 6 zeroes
new Date(order.expiresAt).getTime().toString() + '000000',
}
: undefined,
},
});
if (res?.signature) {
const resId = order.id ?? determineId(res.signature);
setUpdatedOrder(null);
if (resId) {
// Start a subscription looking for the newly created order
subRef.current = client
.subscribe<OrderEvent, OrderEventVariables>({
query: ORDER_EVENT_SUB,
variables: { partyId: keypair?.pub || '' },
})
.subscribe(({ data }) => {
if (!data?.busEvents?.length) {
return;
}
// No types available for the subscription result
const matchingOrderEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Order') {
return false;
}
return e.event.id === resId;
});
if (
matchingOrderEvent &&
matchingOrderEvent.event.__typename === 'Order'
) {
setUpdatedOrder(matchingOrderEvent.event);
subRef.current?.unsubscribe();
}
});
}
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[client, keypair, send]
);
return {
transaction,
updatedOrder,
edit,
reset,
};
};
@@ -5,6 +5,7 @@ import {
} from '@vegaprotocol/wallet';
import { toDecimal } from '@vegaprotocol/react-helpers';
import type { Market } from '../market';
import type { OrderStatus } from '@vegaprotocol/types';
export type Order =
| {
@@ -14,6 +15,9 @@ export type Order =
side: VegaWalletOrderSide;
price?: never;
expiration?: never;
rejectionReason: string | null;
status?: OrderStatus;
market?: Market | null;
}
| {
size: string;
@@ -22,6 +26,9 @@ export type Order =
side: VegaWalletOrderSide;
price?: string;
expiration?: Date;
rejectionReason: string | null;
status?: OrderStatus;
market?: Market | null;
};
export const getDefaultOrder = (market: Market): Order => ({
@@ -29,4 +36,6 @@ export const getDefaultOrder = (market: Market): Order => ({
side: VegaWalletOrderSide.Buy,
timeInForce: VegaWalletOrderTimeInForce.IOC,
size: String(toDecimal(market.positionDecimalPlaces)),
rejectionReason: null,
market: null,
});
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useApolloClient } from '@apollo/client';
import type { OperationVariables } from '@apollo/client';
import type { Subscribe, Pagination, Load } from '../lib/generic-data-provider';
import type { Subscribe, Load } from '../lib/generic-data-provider';
/**
*
@@ -48,9 +48,9 @@ export function useDataProvider<Data, Delta>({
reloadRef.current(force);
}
}, []);
const load = useCallback((pagination: Pagination) => {
const load = useCallback<Load<Data>>((...args) => {
if (loadRef.current) {
return loadRef.current(pagination);
return loadRef.current(...args);
}
return Promise.reject();
}, []);
@@ -0,0 +1,445 @@
import { makeDataProvider, defaultAppend } from './generic-data-provider';
import type {
Query,
UpdateCallback,
Update,
PageInfo,
} from './generic-data-provider';
import type {
ApolloClient,
FetchResult,
SubscriptionOptions,
OperationVariables,
ApolloQueryResult,
QueryOptions,
} from '@apollo/client';
import type { Subscription, Observable } from 'zen-observable-ts';
type Item = {
cursor: string;
node: {
id: string;
};
};
type Data = Item[];
type QueryData = {
data: Data;
pageInfo?: PageInfo;
totalCount?: number;
};
type SubscriptionData = QueryData;
type Delta = Data;
describe('data provider', () => {
const update = jest.fn<
ReturnType<Update<Data, Delta>>,
Parameters<Update<Data, Delta>>
>();
const callback = jest.fn<
ReturnType<UpdateCallback<Data, Delta>>,
Parameters<UpdateCallback<Data, Delta>>
>();
const query: Query<QueryData> = {
kind: 'Document',
definitions: [],
};
const subscriptionQuery: Query<SubscriptionData> = query;
const subscribe = makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
query,
subscriptionQuery,
update,
(r) => r.data,
(r) => r.data
);
const first = 100;
const paginatedSubscribe = makeDataProvider<
QueryData,
Data,
SubscriptionData,
Delta
>(
query,
subscriptionQuery,
update,
(r) => r.data,
(r) => r.data,
{
first,
append: defaultAppend,
getPageInfo: (r) => r?.pageInfo ?? null,
getTotalCount: (r) => r?.totalCount,
}
);
const generateData = (start = 0, size = first) => {
return new Array(size).fill(null).map((v, i) => ({
cursor: (i + start + 1).toString(),
node: {
id: (i + start + 1).toString(),
},
}));
};
const clientSubscribeUnsubscribe = jest.fn();
const clientSubscribeSubscribe = jest.fn<
Subscription,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[(value: FetchResult<SubscriptionData>) => void, (error: any) => void]
>(() => ({
unsubscribe: clientSubscribeUnsubscribe,
closed: false,
}));
const clientSubscribe = jest.fn<
Observable<FetchResult<SubscriptionData>>,
[SubscriptionOptions<OperationVariables, SubscriptionData>]
>(
() =>
({
subscribe: clientSubscribeSubscribe,
} as unknown as Observable<FetchResult<SubscriptionData>>)
);
const clientQueryPromise: {
resolve?: (
value:
| ApolloQueryResult<QueryData>
| PromiseLike<ApolloQueryResult<QueryData>>
) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject?: (reason?: any) => void;
} = {};
const clientQuery = jest.fn<
Promise<ApolloQueryResult<QueryData>>,
[QueryOptions<OperationVariables, QueryData>]
>(() => {
return new Promise((resolve, reject) => {
clientQueryPromise.resolve = resolve;
clientQueryPromise.reject = reject;
});
});
const client = {
query: clientQuery,
subscribe: clientSubscribe,
} as unknown as ApolloClient<object>;
const resolveQuery = async (data: QueryData) => {
if (clientQueryPromise.resolve) {
await clientQueryPromise.resolve({
data,
loading: false,
networkStatus: 8,
});
}
};
it('memoize instance and unsubscribe if no subscribers', () => {
const subscription1 = subscribe(jest.fn(), client);
const subscription2 = subscribe(jest.fn(), client);
expect(clientSubscribeSubscribe.mock.calls.length).toEqual(1);
subscription1.unsubscribe();
expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(0);
subscription2.unsubscribe();
expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(1);
});
it('calls callback before and after initial fetch', async () => {
callback.mockClear();
const data: Item[] = [];
const subscription = subscribe(callback, client);
expect(callback.mock.calls.length).toBe(1);
expect(callback.mock.calls[0][0].data).toBe(null);
expect(callback.mock.calls[0][0].loading).toBe(true);
await resolveQuery({ data });
expect(callback.mock.calls.length).toBe(2);
expect(callback.mock.calls[1][0].data).toBe(data);
expect(callback.mock.calls[1][0].loading).toBe(false);
subscription.unsubscribe();
});
it('calls update and callback on each update', async () => {
const data: Item[] = [];
const subscription = subscribe(callback, client);
await resolveQuery({ data });
const delta: Item[] = [];
update.mockImplementationOnce((data, delta) => [...data, ...delta]);
// calling onNext from client.subscribe({ query }).subscribe(onNext)
await clientSubscribeSubscribe.mock.calls[
clientSubscribeSubscribe.mock.calls.length - 1
][0]({ data: { data: delta } });
expect(update.mock.calls[update.mock.calls.length - 1][0]).toBe(data);
expect(update.mock.calls[update.mock.calls.length - 1][1]).toBe(delta);
expect(callback.mock.calls[callback.mock.calls.length - 1][0].delta).toBe(
delta
);
subscription.unsubscribe();
});
it("don't calls callback on update if data doesn't", async () => {
callback.mockClear();
const data: Item[] = [];
const subscription = subscribe(callback, client);
await resolveQuery({ data });
const delta: Item[] = [];
update.mockImplementationOnce((data, delta) => data);
const callbackCallsLength = callback.mock.calls.length;
// calling onNext from client.subscribe({ query }).subscribe(onNext)
await clientSubscribeSubscribe.mock.calls[
clientSubscribeSubscribe.mock.calls.length - 1
][0]({ data: { data: delta } });
expect(update.mock.calls[update.mock.calls.length - 1][0]).toBe(data);
expect(update.mock.calls[update.mock.calls.length - 1][1]).toBe(delta);
expect(callback.mock.calls.length).toBe(callbackCallsLength);
subscription.unsubscribe();
});
it('refetch data on reload', async () => {
clientQuery.mockClear();
clientSubscribeUnsubscribe.mockClear();
clientSubscribeSubscribe.mockClear();
const data: Item[] = [];
const subscription = subscribe(callback, client);
await resolveQuery({ data });
subscription.reload();
await resolveQuery({ data });
expect(clientQuery.mock.calls.length).toBe(2);
expect(clientSubscribeSubscribe.mock.calls.length).toBe(1);
expect(clientSubscribeUnsubscribe.mock.calls.length).toBe(0);
subscription.unsubscribe();
});
it('refetch data and restart subscription on reload with force', async () => {
clientQuery.mockClear();
clientSubscribeUnsubscribe.mockClear();
clientSubscribeSubscribe.mockClear();
const data: Item[] = [];
const subscription = subscribe(callback, client);
await resolveQuery({ data });
subscription.reload(true);
await resolveQuery({ data });
expect(clientQuery.mock.calls.length).toBe(2);
expect(clientSubscribeSubscribe.mock.calls.length).toBe(2);
expect(clientSubscribeUnsubscribe.mock.calls.length).toBe(1);
subscription.unsubscribe();
});
it('calls callback on flush', async () => {
callback.mockClear();
const data: Item[] = [];
const subscription = subscribe(callback, client);
await resolveQuery({ data });
const callbackCallsLength = callback.mock.calls.length;
subscription.flush();
expect(callback.mock.calls.length).toBe(callbackCallsLength + 1);
subscription.unsubscribe();
});
it('fills data with nulls if paginaton is enabled', async () => {
callback.mockClear();
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);
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 () => {
callback.mockClear();
const totalCount = 1000;
const subscription = paginatedSubscribe(callback, client);
await resolveQuery({
data: generateData(),
totalCount,
pageInfo: {
hasNextPage: true,
endCursor: '100',
},
});
// load next page
subscription.load();
let lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '100',
first,
});
await resolveQuery({
data: generateData(100),
pageInfo: {
hasNextPage: true,
endCursor: '200',
},
});
// load page with skip
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(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(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(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();
expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength);
// load last page longer than expected
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 () => {
callback.mockClear();
const totalCount = undefined;
const subscription = paginatedSubscribe(callback, client);
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();
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();
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);
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();
});
});
@@ -7,6 +7,7 @@ import type {
} from '@apollo/client';
import type { Subscription } from 'zen-observable-ts';
import isEqual from 'lodash/isEqual';
import type { Pagination as PaginationWithoutSkip } from '@vegaprotocol/types';
export interface UpdateCallback<Data, Delta> {
(arg: {
@@ -21,15 +22,12 @@ export interface UpdateCallback<Data, Delta> {
}
export interface Load<Data> {
(pagination: Pagination): Promise<Data | null>;
(start?: number, end?: number): Promise<Data | null>;
}
export interface Pagination {
first?: number;
after?: string;
last?: number;
before?: string;
}
type Pagination = PaginationWithoutSkip & {
skip?: number;
};
export interface PageInfo {
startCursor?: string;
@@ -51,7 +49,7 @@ export interface Subscribe<Data, Delta> {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
export type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
export interface Update<Data, Delta> {
(data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data;
@@ -60,13 +58,13 @@ export interface Update<Data, Delta> {
export interface Append<Data> {
(
data: Data | null,
pageInfo: PageInfo,
insertionData: Data | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination
pagination?: Pagination,
totalCount?: number
): {
data: Data | null;
pageInfo: PageInfo;
totalCount?: number;
};
}
@@ -86,6 +84,46 @@ interface GetDelta<SubscriptionData, Delta> {
(subscriptionData: SubscriptionData): Delta;
}
export function defaultAppend<Data>(
data: Data | null,
insertionData: Data | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination,
totalCount?: number
) {
if (data && insertionData && insertionPageInfo) {
if (!(data instanceof Array) || !(insertionData instanceof Array)) {
throw new Error(
'data needs to be instance of { cursor: string }[] when using pagination'
);
}
if (pagination?.after) {
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),
};
}
}
}
return { data, totalCount };
}
/**
* @param subscriptionQuery query that will be used for subscription
* @param update function that will be execued on each onNext, it should update data base on delta, it can reload data provider
@@ -102,7 +140,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
getDelta: GetDelta<SubscriptionData, Delta>,
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount: GetTotalCount<QueryData>;
getTotalCount?: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
},
@@ -125,7 +163,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
// notify single callback about current state, delta is passes optionally only if notify was invoked onNext
const notify = (
callback: UpdateCallback<Data, Delta>,
dataUpdate?: { delta?: Delta; insertionData?: Data | null }
updateData?: { delta?: Delta; insertionData?: Data | null }
) => {
callback({
data,
@@ -133,26 +171,50 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
loading,
pageInfo,
totalCount,
...dataUpdate,
...updateData,
});
};
// notify all callbacks
const notifyAll = (dataUpdate?: {
const notifyAll = (updateData?: {
delta?: Delta;
insertionData?: Data | null;
}) => {
callbacks.forEach((callback) => notify(callback, dataUpdate));
callbacks.forEach((callback) => notify(callback, updateData));
};
const load = async (params?: Pagination) => {
if (!client || !pagination || !pageInfo) {
const load = async (start?: number, end?: number) => {
if (!client || !pagination || !pageInfo || !(data instanceof Array)) {
return Promise.reject();
}
const paginationVariables: Pagination = params ?? {
const paginationVariables: Pagination = {
first: pagination.first,
after: pageInfo.endCursor,
};
if (start !== undefined) {
if (!start) {
paginationVariables.after = undefined;
} else if (data && data[start - 1]) {
paginationVariables.after = (
data[start - 1] as { cursor: string }
).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: string }
).cursor;
}
}
} else if (!pageInfo.hasNextPage) {
return null;
}
const res = await client.query<QueryData>({
query,
variables: {
@@ -162,15 +224,18 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
fetchPolicy,
});
const insertionData = getData(res.data);
const insertionDataPageInfo = pagination.getPageInfo(res.data);
({ data, pageInfo } = pagination.append(
const insertionPageInfo = pagination.getPageInfo(res.data);
({ data, totalCount } = pagination.append(
data,
pageInfo,
insertionData,
insertionDataPageInfo,
paginationVariables
insertionPageInfo,
paginationVariables,
totalCount
));
totalCount = pagination.getTotalCount(res.data);
pageInfo = insertionPageInfo;
totalCount =
(pagination.getTotalCount && pagination.getTotalCount(res.data)) ??
totalCount;
notifyAll({ insertionData });
return insertionData;
};
@@ -188,9 +253,23 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
fetchPolicy,
});
data = getData(res.data);
if (pagination) {
if (data && pagination) {
if (!(data instanceof Array)) {
throw new Error(
'data needs to be instance of { cursor: string }[] when using pagination'
);
}
pageInfo = pagination.getPageInfo(res.data);
totalCount = pagination.getTotalCount(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 (data && updateQueue && updateQueue.length > 0) {
@@ -255,11 +334,11 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
if (loading || !data) {
updateQueue.push(delta);
} else {
const newData = update(data, delta, reload);
if (newData === data) {
const updatedData = update(data, delta, reload);
if (updatedData === data) {
return;
}
data = newData;
data = updatedData;
notifyAll({ delta });
}
},
@@ -361,7 +440,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
getDelta: GetDelta<SubscriptionData, Delta>,
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount: GetTotalCount<QueryData>;
getTotalCount?: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
},
@@ -30,7 +30,7 @@ export const CumulativeVol = React.memo(
const askBar = relativeAsk ? (
<div
data-testid="ask-bar"
className="absolute left-0 top-0"
className="absolute left-0 top-0 opacity-40 dark:opacity-100"
style={{
height: relativeBid && relativeAsk ? '50%' : '100%',
width: `${relativeAsk}%`,
@@ -41,7 +41,7 @@ export const CumulativeVol = React.memo(
const bidBar = relativeBid ? (
<div
data-testid="bid-bar"
className="absolute top-0 left-0"
className="absolute top-0 left-0 opacity-40 dark:opacity-100"
style={{
height: relativeBid && relativeAsk ? '50%' : '100%',
top: relativeBid && relativeAsk ? '50%' : '0',
+7 -4
View File
@@ -30,10 +30,13 @@ export const Vol = React.memo(
return (
<div className="relative" data-testid={testId || 'vol'}>
<div
className={classNames('h-full absolute top-0', {
'left-0': type === VolumeType.bid,
'right-0': type === VolumeType.ask,
})}
className={classNames(
'h-full absolute top-0 opacity-40 dark:opacity-100',
{
'left-0': type === VolumeType.bid,
'right-0': type === VolumeType.ask,
}
)}
style={{
width: relativeValue ? `${relativeValue}%` : '0%',
backgroundColor: type === VolumeType.bid ? BID_COLOR : ASK_COLOR,
@@ -39,6 +39,58 @@ const vegaCustomClassesLite = plugin(function ({ addUtilities }) {
marginTop: '10px',
marginRight: '5px',
},
'.buyButton': {
textTransform: 'uppercase',
textDecoration: 'none',
backgroundColor: 'rgba(0, 143, 74, 0.1)',
border: `1px solid ${theme.colors.darkerGreen}`,
color: theme.colors.darkerGreen,
paddingTop: '0.5rem',
paddingBottom: '0.5rem',
'&:hover': {
backgroundColor: theme.colors.darkerGreen,
color: theme.colors.white.DEFAULT,
},
'&.selected': {
backgroundColor: theme.colors.darkerGreen,
color: theme.colors.white.DEFAULT,
},
},
'.buyButtonDark': {
color: theme.colors.darkerGreen,
'&:hover': {
color: theme.colors.black.DEFAULT,
},
'&.selected': {
color: theme.colors.black.DEFAULT,
},
},
'.sellButton': {
textTransform: 'uppercase',
textDecoration: 'none',
paddingTop: '0.5rem',
paddingBottom: '0.5rem',
backgroundColor: 'rgba(255, 8, 126, 0.1)',
border: `1px solid ${theme.colors.pink}`,
color: theme.colors.pink,
'&:hover': {
color: theme.colors.white.DEFAULT,
backgroundColor: theme.colors.pink,
},
'&.selected': {
backgroundColor: theme.colors.pink,
color: theme.colors.white.DEFAULT,
},
},
'.sellButtonDark': {
color: theme.colors.pink,
'&:hover': {
color: theme.colors.black.DEFAULT,
},
'&.selected': {
color: theme.colors.black.DEFAULT,
},
},
});
});
+33 -8
View File
@@ -3,11 +3,13 @@
// @generated
// This file was automatically generated and should not be edited.
import { Pagination } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: Trades
// ====================================================
export interface Trades_market_trades_market {
export interface Trades_market_tradesConnection_edges_node_market {
__typename: "Market";
/**
* Market ID
@@ -38,7 +40,7 @@ export interface Trades_market_trades_market {
positionDecimalPlaces: number;
}
export interface Trades_market_trades {
export interface Trades_market_tradesConnection_edges_node {
__typename: "Trade";
/**
* The hash of the trade data
@@ -59,7 +61,33 @@ export interface Trades_market_trades {
/**
* The market the trade occurred on
*/
market: Trades_market_trades_market;
market: Trades_market_tradesConnection_edges_node_market;
}
export interface Trades_market_tradesConnection_edges {
__typename: "TradeEdge";
node: Trades_market_tradesConnection_edges_node;
cursor: string;
}
export interface Trades_market_tradesConnection_pageInfo {
__typename: "PageInfo";
startCursor: string;
endCursor: string;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
export interface Trades_market_tradesConnection {
__typename: "TradeConnection";
/**
* The trade in this connection
*/
edges: Trades_market_tradesConnection_edges[];
/**
* The pagination information
*/
pageInfo: Trades_market_tradesConnection_pageInfo;
}
export interface Trades_market {
@@ -68,10 +96,7 @@ export interface Trades_market {
* Market ID
*/
id: string;
/**
* Trades on a market
*/
trades: Trades_market_trades[] | null;
tradesConnection: Trades_market_tradesConnection;
}
export interface Trades {
@@ -83,5 +108,5 @@ export interface Trades {
export interface TradesVariables {
marketId: string;
maxTrades: number;
pagination?: Pagination | null;
}
+124 -37
View File
@@ -1,16 +1,22 @@
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { GridApi } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useRef } from 'react';
import type {
IGetRowsParams,
BodyScrollEvent,
BodyScrollEndEvent,
} from 'ag-grid-community';
import {
MAX_TRADES,
sortTrades,
tradesDataProvider as dataProvider,
} from './trades-data-provider';
import { TradesTable } from './trades-table';
import type { TradeFields } from './__generated__/TradeFields';
import type { TradesVariables } from './__generated__/Trades';
import type {
TradesVariables,
Trades_market_tradesConnection_edges,
} from './__generated__/Trades';
interface TradesContainerProps {
marketId: string;
@@ -18,51 +24,132 @@ interface TradesContainerProps {
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const dataRef = useRef<
(Trades_market_tradesConnection_edges | null)[] | null
>(null);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const scrolledToTop = useRef(true);
const variables = useMemo<TradesVariables>(
() => ({ marketId, maxTrades: MAX_TRADES }),
[marketId]
);
const update = useCallback(({ delta }: { delta: TradeFields[] }) => {
if (!gridRef.current?.api) {
return false;
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
const incoming = sortTrades(delta);
const currentRows = getAllRows(gridRef.current.api);
// Create array of trades whose index is now greater than the max so we
// can remove them from the grid
const outgoing = [...incoming, ...currentRows].filter(
(r, i) => i > MAX_TRADES - 1
);
gridRef.current.api.applyTransactionAsync({
add: incoming,
remove: outgoing,
addIndex: 0,
});
return true;
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
if (!gridRef.current?.api) {
return;
}
gridRef.current.api.refreshInfiniteCache();
}, []);
const { data, error, loading } = useDataProvider({
const update = useCallback(
({
data,
delta,
}: {
data: (Trades_market_tradesConnection_edges | null)[];
delta: TradeFields[];
}) => {
if (!gridRef.current?.api) {
return false;
}
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += delta.filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
},
[]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: (Trades_market_tradesConnection_edges | null)[];
totalCount?: number;
}) => {
dataRef.current = data;
totalCountRef.current = totalCount;
return true;
},
[]
);
const { data, error, loading, load, totalCount } = useDataProvider({
dataProvider,
update,
insert,
variables,
});
totalCountRef.current = totalCount;
dataRef.current = data;
const getRows = async ({
successCallback,
failCallback,
startRow,
endRow,
}: IGetRowsParams) => {
startRow += newRows.current;
endRow += newRows.current;
try {
if (dataRef.current && dataRef.current.indexOf(null) < endRow) {
await load();
}
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow).map((edge) => edge?.node)
: [];
let lastRow = -1;
if (totalCountRef.current !== undefined) {
if (!totalCountRef.current) {
lastRow = 0;
} else if (totalCountRef.current <= endRow) {
lastRow = totalCountRef.current;
}
} else if (rowsThisBlock.length < endRow - startRow) {
lastRow = rowsThisBlock.length;
}
successCallback(rowsThisBlock, lastRow);
} catch (e) {
failCallback();
}
};
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) {
addNewRows();
}
};
const onBodyScroll = (event: BodyScrollEvent) => {
scrolledToTop.current = event.top <= 0;
};
return (
<AsyncRenderer
loading={loading}
error={error}
data={data}
render={(data) => <TradesTable ref={gridRef} data={data} />}
/>
<AsyncRenderer loading={loading} error={error} data={data}>
<TradesTable
ref={gridRef}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
/>
</AsyncRenderer>
);
};
const getAllRows = (api: GridApi) => {
const rows: TradeFields[] = [];
api.forEachNode((node) => {
rows.push(node.data);
});
return rows;
};
+57 -28
View File
@@ -1,7 +1,15 @@
import { gql } from '@apollo/client';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import {
makeDataProvider,
defaultAppend as append,
} from '@vegaprotocol/react-helpers';
import type { PageInfo } from '@vegaprotocol/react-helpers';
import type { TradeFields } from './__generated__/TradeFields';
import type { Trades } from './__generated__/Trades';
import type {
Trades,
Trades_market_tradesConnection_edges,
Trades_market_tradesConnection_edges_node,
} from './__generated__/Trades';
import type { TradesSub } from './__generated__/TradesSub';
import orderBy from 'lodash/orderBy';
import produce from 'immer';
@@ -24,11 +32,22 @@ const TRADES_FRAGMENT = gql`
export const TRADES_QUERY = gql`
${TRADES_FRAGMENT}
query Trades($marketId: ID!, $maxTrades: Int!) {
query Trades($marketId: ID!, $pagination: Pagination) {
market(id: $marketId) {
id
trades(last: $maxTrades) {
...TradeFields
tradesConnection(pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
}
@@ -43,40 +62,50 @@ export const TRADES_SUB = gql`
}
`;
export const sortTrades = (trades: TradeFields[]) => {
return orderBy(
trades,
(t) => {
return new Date(t.createdAt).getTime();
},
'desc'
);
};
const update = (data: TradeFields[], delta: TradeFields[]) => {
const update = (
data: (Trades_market_tradesConnection_edges | null)[],
delta: TradeFields[]
) => {
return produce(data, (draft) => {
const incoming = sortTrades(delta);
// Add new trades to the top
draft.unshift(...incoming);
// Remove old trades from the bottom
if (draft.length > MAX_TRADES) {
draft.splice(MAX_TRADES, draft.length - MAX_TRADES);
}
orderBy(delta, 'createdAt', 'desc').forEach((node) => {
const index = draft.findIndex((edge) => edge?.node.id === node.id);
if (index !== -1) {
if (draft[index]?.node) {
Object.assign(
draft[index]?.node as Trades_market_tradesConnection_edges_node,
node
);
}
} else {
const firstNode = draft[0]?.node;
if (firstNode && node.createdAt >= firstNode.createdAt) {
draft.unshift({ node, cursor: '', __typename: 'TradeEdge' });
}
}
});
});
};
const getData = (responseData: Trades): TradeFields[] | null =>
responseData.market ? responseData.market.trades : null;
const getData = (
responseData: Trades
): Trades_market_tradesConnection_edges[] | null =>
responseData.market ? responseData.market.tradesConnection.edges : null;
const getDelta = (subscriptionData: TradesSub): TradeFields[] =>
subscriptionData?.trades || [];
const getPageInfo = (responseData: Trades): PageInfo | null =>
responseData.market?.tradesConnection.pageInfo || null;
export const tradesDataProvider = makeDataProvider(
TRADES_QUERY,
TRADES_SUB,
update,
getData,
getDelta
getDelta,
{
getPageInfo,
append,
first: 100,
}
);
+3 -3
View File
@@ -19,7 +19,7 @@ const trade: TradeFields = {
it('Correct columns are rendered', async () => {
await act(async () => {
render(<TradesTable data={[trade]} />);
render(<TradesTable rowData={[trade]} />);
});
const expectedHeaders = ['Price', 'Size', 'Created at'];
const headers = screen.getAllByRole('columnheader');
@@ -29,7 +29,7 @@ it('Correct columns are rendered', async () => {
it('Number and data columns are formatted', async () => {
await act(async () => {
render(<TradesTable data={[trade]} />);
render(<TradesTable rowData={[trade]} />);
});
const cells = screen.getAllByRole('gridcell');
@@ -51,7 +51,7 @@ it('Price and size columns are formatted', async () => {
size: (Number(trade.size) - 10).toString(),
};
await act(async () => {
render(<TradesTable data={[trade2, trade]} />);
render(<TradesTable rowData={[trade2, trade]} />);
});
const cells = screen.getAllByRole('gridcell');
+70 -56
View File
@@ -1,8 +1,7 @@
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { forwardRef, useMemo } from 'react';
import { forwardRef } from 'react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import type { TradeFields } from './__generated__/TradeFields';
import {
addDecimal,
addDecimalsFormatNumber,
@@ -10,8 +9,9 @@ import {
t,
} from '@vegaprotocol/react-helpers';
import type { CellClassParams, ValueFormatterParams } from 'ag-grid-community';
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
import type { Trades_market_tradesConnection_edges_node } from './__generated__/Trades';
import BigNumber from 'bignumber.js';
import { sortTrades } from './trades-data-provider';
export const UP_CLASS = 'text-vega-green';
export const DOWN_CLASS = 'text-vega-red';
@@ -37,58 +37,72 @@ const changeCellClass =
return ['font-mono', colorClass].join(' ');
};
interface TradesTableProps {
data: TradeFields[] | null;
}
type Props = AgGridReactProps | AgReactUiProps;
type TradesTableValueFormatterParams = Omit<
ValueFormatterParams,
'data' | 'value'
> & {
data: Trades_market_tradesConnection_edges_node | null;
};
export const TradesTable = forwardRef<AgGridReact, TradesTableProps>(
({ data }, ref) => {
// Sort initial trades
const trades = useMemo(() => {
if (!data) {
return null;
}
return sortTrades(data);
}, [data]);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No trades')}
rowData={trades}
getRowId={({ data }) => data.id}
ref={ref}
defaultColDef={{
resizable: true,
export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No trades')}
getRowId={({ data }) => data.id}
ref={ref}
defaultColDef={{
resizable: true,
}}
{...props}
>
<AgGridColumn
headerName={t('Price')}
field="price"
width={130}
cellClass={changeCellClass('price')}
valueFormatter={({
value,
data,
}: TradesTableValueFormatterParams & {
value: Trades_market_tradesConnection_edges_node['price'];
}) => {
if (!data?.market) {
return null;
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
}}
>
<AgGridColumn
headerName={t('Price')}
field="price"
width={130}
cellClass={changeCellClass('price')}
valueFormatter={({ value, data }: ValueFormatterParams) => {
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
headerName={t('Size')}
field="size"
width={125}
valueFormatter={({ value, data }: ValueFormatterParams) => {
return addDecimal(value, data.market.positionDecimalPlaces);
}}
cellClass={changeCellClass('size')}
/>
<AgGridColumn
headerName={t('Created at')}
field="createdAt"
width={170}
valueFormatter={({ value }: ValueFormatterParams) => {
return getDateTimeFormat().format(new Date(value));
}}
/>
</AgGrid>
);
}
);
/>
<AgGridColumn
headerName={t('Size')}
field="size"
width={125}
valueFormatter={({
value,
data,
}: TradesTableValueFormatterParams & {
value: Trades_market_tradesConnection_edges_node['size'];
}) => {
if (!data?.market) {
return null;
}
return addDecimal(value, data.market.positionDecimalPlaces);
}}
cellClass={changeCellClass('size')}
/>
<AgGridColumn
headerName={t('Created at')}
field="createdAt"
width={170}
valueFormatter={({
value,
}: TradesTableValueFormatterParams & {
value: Trades_market_tradesConnection_edges_node['createdAt'];
}) => {
return value && getDateTimeFormat().format(new Date(value));
}}
/>
</AgGrid>
);
});
-10
View File
@@ -293,16 +293,6 @@ export enum WithdrawalStatus {
Rejected = "Rejected",
}
/**
* Pagination constructs to support cursor based pagination in the API
*/
export interface Pagination {
first?: number | null;
after?: string | null;
last?: number | null;
before?: string | null;
}
//==============================================================
// END Enums and Input Objects
//==============================================================
+1
View File
@@ -0,0 +1 @@
export * from './globalTypes';
+2 -1
View File
@@ -1,2 +1,3 @@
export * from './__generated__/globalTypes';
export * from './__generated__';
export * from './candle';
export * from './pagination';
+6
View File
@@ -0,0 +1,6 @@
export interface Pagination {
first?: number;
after?: string;
last?: number;
before?: string;
}
@@ -1,6 +1,7 @@
import type { SelectHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import classNames from 'classnames';
import { Icon } from '..';
import { defaultFormElement } from '../../utils/shared';
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
@@ -12,10 +13,17 @@ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ className, hasError, ...props }, ref) => (
<select
ref={ref}
{...props}
className={classNames(defaultFormElement(hasError), className, 'h-28')}
/>
<div className="flex items-center relative">
<select
ref={ref}
{...props}
className={classNames(
defaultFormElement(hasError),
className,
'appearance-none h-28 pr-28'
)}
/>
<Icon name="chevron-down" className="absolute right-8 z-10" />
</div>
)
);
+1 -1
View File
@@ -60,7 +60,7 @@ export const Tabs = ({ children }: TabsProps) => {
return (
<TabsPrimitive.Content
value={child.props.id}
className="h-full"
className="h-full bg-white dark:bg-black"
data-testid={`tab-${child.props.id}`}
>
{child.props.children}
@@ -4,7 +4,11 @@ import type { VegaTxState } from '../use-vega-transaction';
import { VegaTxStatus } from '../use-vega-transaction';
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import {
addDecimalsFormatNumber,
formatLabel,
t,
} from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
import { OrderType } from '@vegaprotocol/types';
import type { Order } from '../wallet-types';
@@ -17,6 +21,7 @@ export interface VegaTransactionDialogProps {
transaction: VegaTxState;
reset: () => void;
title?: string;
children?: ReactNode;
}
const getDialogIntent = (
@@ -45,6 +50,7 @@ export const VegaTransactionDialog = ({
transaction,
reset,
title = '',
children,
}: VegaTransactionDialogProps) => {
// open / close dialog
useEffect(() => {
@@ -75,6 +81,7 @@ export const VegaTransactionDialog = ({
transaction={transaction}
finalizedOrder={finalizedOrder}
title={title}
children={children}
/>
</Dialog>
);
@@ -84,15 +91,22 @@ interface VegaDialogProps {
transaction: VegaTxState;
finalizedOrder: Order | null;
title: string;
children?: ReactNode;
}
export const VegaDialog = ({
transaction,
finalizedOrder,
title,
children,
}: VegaDialogProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
const headerClassName = 'text-h5 font-bold text-black dark:text-white';
if (children && transaction.status === VegaTxStatus.Default) {
return <div>{children}</div>;
}
// Rejected by wallet
if (transaction.status === VegaTxStatus.Requested) {
return (
@@ -159,7 +173,8 @@ export const VegaDialog = ({
icon={<Icon name="warning-sign" size={20} />}
>
<p data-testid="error-reason">
{t(`Reason: ${finalizedOrder.rejectionReason}`)}
{finalizedOrder.rejectionReason &&
t(`Reason: ${formatLabel(finalizedOrder.rejectionReason)}`)}
</p>
</OrderDialogWrapper>
);
@@ -178,6 +193,17 @@ export const VegaDialog = ({
<p className={headerClassName}>{t(`Status`)}</p>
<p>{t(`${finalizedOrder.status}`)}</p>
</div>
{finalizedOrder.type === OrderType.Limit && finalizedOrder.market && (
<div>
<p className={headerClassName}>{t(`Price`)}</p>
<p>
{addDecimalsFormatNumber(
finalizedOrder.price,
finalizedOrder.market.decimalPlaces
)}
</p>
</div>
)}
<div>
<p className={headerClassName}>{t(`Amount`)}</p>
<p
@@ -193,17 +219,6 @@ export const VegaDialog = ({
`}
</p>
</div>
{finalizedOrder.type === OrderType.Limit && finalizedOrder.market && (
<div>
<p className={headerClassName}>{t(`Price`)}</p>
<p>
{addDecimalsFormatNumber(
finalizedOrder.price,
finalizedOrder.market.decimalPlaces
)}
</p>
</div>
)}
</div>
<div className="grid grid-cols-1 gap-8">
{transaction.txHash && (
@@ -231,7 +246,7 @@ interface OrderDialogWrapperProps {
title: string;
}
const OrderDialogWrapper = ({
export const OrderDialogWrapper = ({
children,
icon,
title,
+7 -2
View File
@@ -1,3 +1,4 @@
import type { OrderTimeInForce } from '@vegaprotocol/types';
import type {
DelegateSubmissionBody,
OrderCancellationBody,
@@ -41,14 +42,18 @@ export interface Market {
name: string;
positionDecimalPlaces?: number;
decimalPlaces: number;
id?: string;
}
export interface Order {
status: string;
rejectionReason: string | null;
id?: string;
status?: string;
rejectionReason?: string | null;
size: string;
price: string;
market: Market | null;
type: string | null;
side?: string;
timeInForce: OrderTimeInForce;
expiresAt?: Date | string | null;
}
+1
View File
@@ -9,3 +9,4 @@ export * from './lib/use-ethereum-transaction';
export * from './lib/transaction-dialog';
export * from './lib/web3-provider';
export * from './lib/web3-connect-dialog';
export * from './lib/web3-wallet-input';
+43
View File
@@ -0,0 +1,43 @@
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { useWeb3React } from '@web3-react/core';
import { t } from '@vegaprotocol/react-helpers';
import { Button, Input, Dialog } from '@vegaprotocol/ui-toolkit';
type Web3WalletInputProps = {
inputProps: Partial<
Omit<
ComponentProps<typeof Input>,
'appendIconName' | 'prependIconName' | 'appendElement' | 'prependElement'
>
>;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
export const Web3WalletInput = ({ inputProps }: Web3WalletInputProps) => {
const [isDialogOpen, setDialogOpen] = useState(false);
const { account, connector } = useWeb3React();
return (
<>
<Input
{...inputProps}
appendIconName="chevron-down"
className="cursor-pointer select-none"
onChange={noop}
onClick={() => setDialogOpen(true)}
/>
<Dialog open={isDialogOpen} onChange={setDialogOpen}>
<p className="mb-16">
{t('Connected with ')}
<span className="font-mono">{account}</span>
</p>
<Button onClick={() => connector.deactivate()}>
{t('Disconnect Ethereum Wallet')}
</Button>
</Dialog>
</>
);
};
@@ -20,9 +20,11 @@ export const useGetWithdrawLimits = (asset?: Asset) => {
if (!data || !asset) return null;
const max = new BigNumber(addDecimal(data.toString(), asset.decimals));
const value = new BigNumber(addDecimal(data.toString(), asset.decimals));
const max = value.isEqualTo(0)
? new BigNumber(Infinity)
: value.minus(new BigNumber(addDecimal('1', asset.decimals)));
return {
max: max.isEqualTo(0) ? new BigNumber(Infinity) : max,
max,
};
};
+93 -75
View File
@@ -1,14 +1,20 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { useWeb3React } from '@web3-react/core';
import { WithdrawForm } from './withdraw-form';
import type { WithdrawFormProps } from './withdraw-form';
import { generateAsset } from './test-helpers';
import type { Asset } from './types';
import type { WithdrawFormProps } from './withdraw-form';
const ethereumAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
jest.mock('@web3-react/core');
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
let assets: Asset[];
let props: WithdrawFormProps;
beforeEach(() => {
const assets = [
assets = [
generateAsset(),
generateAsset({
id: 'asset-id-2',
@@ -16,96 +22,108 @@ beforeEach(() => {
name: 'asset-name-2',
}),
];
props = {
assets,
min: new BigNumber(0.00001),
max: new BigNumber(100),
max: {
balance: new BigNumber(100),
threshold: new BigNumber(200),
},
limits: {
max: new BigNumber(200),
},
ethereumAccount: undefined,
selectedAsset: undefined,
onSelectAsset: jest.fn(),
submitWithdraw: jest.fn().mockReturnValue(Promise.resolve()),
};
(useWeb3React as jest.Mock).mockReturnValue({ account: MOCK_ETH_ADDRESS });
});
const generateJsx = (props: WithdrawFormProps) => <WithdrawForm {...props} />;
describe('Withdrawal form', () => {
it('renders with default values', async () => {
render(<WithdrawForm {...props} />);
it('Validation', async () => {
const { rerender } = render(generateJsx(props));
fireEvent.submit(screen.getByTestId('withdraw-form'));
expect(await screen.findAllByRole('alert')).toHaveLength(3);
expect(screen.getAllByText('Required')).toHaveLength(3);
// Selected asset state lives in state so rerender with it now selected
rerender(generateJsx({ ...props, selectedAsset: props.assets[0] }));
fireEvent.change(screen.getByLabelText('Asset'), {
target: { value: props.assets[0].id },
expect(screen.getByLabelText('Asset')).toHaveValue('');
expect(screen.getByLabelText('To (Ethereum address)')).toHaveValue(
MOCK_ETH_ADDRESS
);
expect(screen.getByLabelText('Amount')).toHaveValue(null);
});
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
target: { value: 'invalid-address' },
describe('field validation', () => {
it('fails when submitted with empty required fields', async () => {
render(<WithdrawForm {...props} />);
fireEvent.submit(screen.getByTestId('withdraw-form'));
expect(await screen.findAllByRole('alert')).toHaveLength(2);
expect(screen.getAllByText('Required')).toHaveLength(2);
});
it('fails when submitted with invalid ethereum address', async () => {
(useWeb3React as jest.Mock).mockReturnValue({ account: '123' });
render(<WithdrawForm {...props} selectedAsset={props.assets[0]} />);
fireEvent.change(screen.getByLabelText('Asset'), {
target: { value: props.assets[0].id },
});
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '101' },
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
expect(
await screen.findByText('Invalid Ethereum address')
).toBeInTheDocument();
expect(
screen.getByText('Insufficient amount in account')
).toBeInTheDocument();
});
it('fails when submitted amount is less than the minimum limit', async () => {
render(<WithdrawForm {...props} selectedAsset={props.assets[0]} />);
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.000000000001' },
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
});
it('passes validation with correct field values', async () => {
render(<WithdrawForm {...props} selectedAsset={props.assets[0]} />);
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '40' },
});
await act(async () => {
fireEvent.submit(screen.getByTestId('withdraw-form'));
});
expect(props.submitWithdraw).toHaveBeenCalledWith({
asset: props.assets[0].id,
amount: '4000000',
receiverAddress: MOCK_ETH_ADDRESS,
});
});
});
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '101' },
});
it('populates amount field with balance value when clicking the "use maximum" button', () => {
const asset = props.assets[0];
render(<WithdrawForm {...props} selectedAsset={asset} />);
fireEvent.submit(screen.getByTestId('withdraw-form'));
fireEvent.click(screen.getByText('Use maximum'));
expect(
await screen.findByText('Invalid Ethereum address')
).toBeInTheDocument();
expect(screen.getByText('Value is above maximum')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
target: { value: ethereumAddress },
});
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.000000000001' },
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '40' },
});
await act(async () => {
fireEvent.submit(screen.getByTestId('withdraw-form'));
});
expect(props.submitWithdraw).toHaveBeenCalledWith({
asset: props.assets[0].id,
amount: '4000000',
receiverAddress: ethereumAddress,
expect(screen.getByLabelText('Amount')).toHaveValue(
Number(props.max.balance.toFixed(asset.decimals))
);
});
});
it('Use max button', () => {
const asset = props.assets[0];
render(generateJsx({ ...props, selectedAsset: asset }));
fireEvent.click(screen.getByText('Use maximum'));
expect(screen.getByLabelText('Amount')).toHaveValue(
Number(props.max.toFixed(asset.decimals))
);
});
it('Use connected Ethereum account', () => {
render(generateJsx({ ...props, ethereumAccount: ethereumAddress }));
fireEvent.click(screen.getByText('Use connected'));
expect(screen.getByLabelText('To (Ethereum address)')).toHaveValue(
ethereumAddress
);
});
+28 -25
View File
@@ -1,10 +1,10 @@
import {
ethereumAddress,
maxSafe,
minSafe,
t,
removeDecimal,
required,
maxSafe,
} from '@vegaprotocol/react-helpers';
import {
Button,
@@ -13,7 +13,9 @@ import {
InputError,
Select,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { Web3WalletInput } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useForm, Controller } from 'react-hook-form';
import type { WithdrawalFields } from './use-withdraw';
@@ -28,10 +30,12 @@ interface FormFields {
export interface WithdrawFormProps {
assets: Asset[];
max: BigNumber;
max: {
balance: BigNumber;
threshold: BigNumber;
};
min: BigNumber;
selectedAsset?: Asset;
ethereumAccount?: string;
limits: {
max: BigNumber;
} | null;
@@ -44,11 +48,11 @@ export const WithdrawForm = ({
max,
min,
selectedAsset,
ethereumAccount,
limits,
onSelectAsset,
submitWithdraw,
}: WithdrawFormProps) => {
const { account: address } = useWeb3React();
const {
register,
handleSubmit,
@@ -59,7 +63,7 @@ export const WithdrawForm = ({
} = useForm<FormFields>({
defaultValues: {
asset: selectedAsset?.id,
to: ethereumAccount,
to: address,
},
});
const onSubmit = async (fields: FormFields) => {
@@ -105,7 +109,6 @@ export const WithdrawForm = ({
</Select>
)}
/>
{errors.asset?.message && (
<InputError intent="danger" className="mt-4">
{errors.asset.message}
@@ -117,31 +120,21 @@ export const WithdrawForm = ({
labelFor="ethereum-address"
className="relative"
>
<Input
{...register('to', { validate: { required, ethereumAddress } })}
id="ethereum-address"
autoComplete="off"
<Web3WalletInput
inputProps={{
id: 'ethereum-address',
...register('to', { validate: { required, ethereumAddress } }),
}}
/>
{errors.to?.message && (
<InputError intent="danger" className="mt-4">
{errors.to.message}
</InputError>
)}
{ethereumAccount && (
<UseButton
data-testid="use-connected"
onClick={() => {
setValue('to', ethereumAccount);
clearErrors('to');
}}
>
{t('Use connected')}
</UseButton>
)}
</FormGroup>
{selectedAsset && limits && (
<div className="mb-20">
<WithdrawLimits limits={limits} />
<WithdrawLimits limits={limits} balance={max.balance} />
</div>
)}
<FormGroup label={t('Amount')} labelFor="amount" className="relative">
@@ -152,7 +145,17 @@ export const WithdrawForm = ({
{...register('amount', {
validate: {
required,
maxSafe: (value) => maxSafe(max)(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max.balance)) {
return t('Insufficient amount in account');
} else if (value.isGreaterThan(max.threshold)) {
return t('Amount is above temporary withdrawal limit');
}
return maxSafe(BigNumber.minimum(max.balance, max.threshold))(
v
);
},
minSafe: (value) => minSafe(min)(value),
},
})}
@@ -166,7 +169,7 @@ export const WithdrawForm = ({
<UseButton
data-testid="use-maximum"
onClick={() => {
setValue('amount', max.toFixed(selectedAsset.decimals));
setValue('amount', max.balance.toFixed(selectedAsset.decimals));
clearErrors('amount');
}}
>
+14 -12
View File
@@ -5,9 +5,10 @@ interface WithdrawLimitsProps {
limits: {
max: BigNumber;
};
balance: BigNumber;
}
export const WithdrawLimits = ({ limits }: WithdrawLimitsProps) => {
export const WithdrawLimits = ({ limits, balance }: WithdrawLimitsProps) => {
let maxLimit = '';
if (limits.max.isEqualTo(Infinity)) {
@@ -19,16 +20,17 @@ export const WithdrawLimits = ({ limits }: WithdrawLimitsProps) => {
}
return (
<>
<p className="text-ui font-bold">{t('Withdraw limits')}</p>
<table className="w-full text-ui">
<tbody>
<tr>
<th className="text-left font-normal">{t('Maximum')}</th>
<td className="text-right">{maxLimit}</td>
</tr>
</tbody>
</table>
</>
<table className="w-full text-ui">
<tbody>
<tr>
<th className="text-left font-normal">{t('Balance available')}</th>
<td className="text-right">{balance.toString()}</td>
</tr>
<tr>
<th className="text-left font-normal">{t('Maximum withdrawal')}</th>
<td className="text-right">{maxLimit}</td>
</tr>
</tbody>
</table>
);
};
@@ -111,7 +111,9 @@ it('Correct min max values provided to form', async () => {
target: { value: '2' },
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
expect(await screen.findByText('Value is above maximum')).toBeInTheDocument();
expect(
await screen.findByText('Insufficient amount in account')
).toBeInTheDocument();
expect(mockSubmit).not.toBeCalled();
});
+17 -11
View File
@@ -9,7 +9,6 @@ import { addDecimal } from '@vegaprotocol/react-helpers';
import { AccountType } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import type { Account, Asset } from './types';
import { useWeb3React } from '@web3-react/core';
import { useGetWithdrawLimits } from './use-get-withdraw-limits';
export interface WithdrawManagerProps {
@@ -29,7 +28,6 @@ export const WithdrawManager = ({
const [dialogOpen, setDialogOpen] = useState(false);
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId);
const { account: ethereumAccount } = useWeb3React();
const { ethTx, vegaTx, approval, submit, reset } = useWithdraw(
dialogDismissed.current,
isNewContract
@@ -40,22 +38,31 @@ export const WithdrawManager = ({
return assets?.find((a) => a.id === assetId);
}, [assets, assetId]);
const account = useMemo(() => {
return accounts.find(
(a) => a.type === AccountType.General && a.asset.id === asset?.id
);
}, [asset, accounts]);
const limits = useGetWithdrawLimits(asset);
const max = useMemo(() => {
if (!asset) {
return new BigNumber(0);
return {
balance: new BigNumber(0),
threshold: new BigNumber(0),
};
}
const account = accounts.find(
(a) => a.type === AccountType.General && a.asset.id === asset.id
);
const v = account
const balance = account
? new BigNumber(addDecimal(account.balance, asset.decimals))
: new BigNumber(0);
return BigNumber.minimum(v, limits ? limits.max : new BigNumber(Infinity));
}, [asset, accounts, limits]);
return {
balance,
threshold: limits ? limits.max : new BigNumber(Infinity),
};
}, [asset, account, limits]);
const min = useMemo(() => {
return asset
@@ -86,7 +93,6 @@ export const WithdrawManager = ({
return (
<>
<WithdrawForm
ethereumAccount={ethereumAccount}
selectedAsset={asset}
onSelectAsset={(id) => setAssetId(id)}
assets={sortBy(assets, 'name')}
+2 -2
View File
@@ -6,7 +6,7 @@
"start": "nx serve",
"build": "nx build",
"test": "nx test",
"postinstall": "husky install && yarn tsc -b tools/executors/**"
"postinstall": "husky install && yarn tsc -b tools/executors/next && yarn tsc -b tools/executors/webpack"
},
"engines": {
"node": ">=16.14.0"
@@ -30,7 +30,7 @@
"@sentry/react": "^6.19.2",
"@sentry/tracing": "^6.19.2",
"@testing-library/user-event": "^14.2.1",
"@vegaprotocol/vegawallet-service-api-client": "0.4.14",
"@vegaprotocol/vegawallet-service-api-client": "0.4.15",
"@walletconnect/ethereum-provider": "^1.7.5",
"@web3-react/core": "8.0.20-beta.0",
"@web3-react/metamask": "8.0.16-beta.0",
+4 -4
View File
@@ -6693,10 +6693,10 @@
"@typescript-eslint/types" "5.22.0"
eslint-visitor-keys "^3.0.0"
"@vegaprotocol/vegawallet-service-api-client@0.4.14":
version "0.4.14"
resolved "https://registry.yarnpkg.com/@vegaprotocol/vegawallet-service-api-client/-/vegawallet-service-api-client-0.4.14.tgz#cdec296644380f95397688e10b753af328c38147"
integrity sha512-xQ/Dg4Bg+3LSHybYHV83i3G7i407Jj8ROElblZ2TTHTW9iHbBhbd/EHtWfUF2C6R6U27+JUZExnFPcZlvNXprA==
"@vegaprotocol/vegawallet-service-api-client@0.4.15":
version "0.4.15"
resolved "https://registry.yarnpkg.com/@vegaprotocol/vegawallet-service-api-client/-/vegawallet-service-api-client-0.4.15.tgz#b303fec121b9b334a678161a6f66b360aeed5f0d"
integrity sha512-YwJkUgFvFqpA1xPYQ30ILGddgzjwD9lclsu1GvwK2AUX/8e3iUcXyr37wLd/t8mDZ7P3Zb2AsuLJP8uZ6E1GHQ==
dependencies:
es6-promise "^4.2.4"
url-parse "^1.4.3"