Merge branch 'master' of github.com:vegaprotocol/frontend-monorepo
This commit is contained in:
@@ -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
|
||||
######
|
||||
|
||||
@@ -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
|
||||
######
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
+121
-3
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
+5
-1
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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
|
||||
//==============================================================
|
||||
|
||||
Generated
+1
@@ -0,0 +1 @@
|
||||
export * from './globalTypes';
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './__generated__/globalTypes';
|
||||
export * from './__generated__';
|
||||
export * from './candle';
|
||||
export * from './pagination';
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user