Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2666ffe462 | ||
|
|
51187029aa | ||
|
|
f7f7be59d3 | ||
|
|
49e3baf094 | ||
|
|
a575b4c502 | ||
|
|
af2e52d59c | ||
|
|
2725bef159 | ||
|
|
070f1905e2 | ||
|
|
6705eb4398 | ||
|
|
9d3fc04597 | ||
|
|
d3929b8d4a | ||
|
|
8a5579b1cc | ||
|
|
197f2e8097 | ||
|
|
8f5a2276de | ||
|
|
46513685c8 | ||
|
|
e66e96f12d | ||
|
|
31c8365812 | ||
|
|
460f534c7a | ||
|
|
24cd080dd4 | ||
|
|
f10c33748f | ||
|
|
ac163d0194 | ||
|
|
e7616e64f8 | ||
|
|
adca4600c2 | ||
|
|
a8eef1cb53 | ||
|
|
58d4bd1459 | ||
|
|
26020b5f71 | ||
|
|
16d345eb4a | ||
|
|
3b3fcab4f4 |
@@ -16,8 +16,6 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# run copies of the current job in parallel
|
||||
containers: [1, 2]
|
||||
project: ${{ fromJSON(inputs.projects) }}
|
||||
runs-on: self-hosted-runner
|
||||
timeout-minutes: 30
|
||||
@@ -66,7 +64,7 @@ jobs:
|
||||
######
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --parallel --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
@@ -89,7 +87,7 @@ jobs:
|
||||
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: logs-${{ matrix.project }}
|
||||
path: /home/runner/.vegacapsule/testnet/logs
|
||||
|
||||
@@ -4,9 +4,8 @@ import {
|
||||
getMarketExpiryDateFormatted,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { MarketInfoNoCandlesQuery } from '@vegaprotocol/market-info';
|
||||
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
|
||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
||||
import pick from 'lodash/pick';
|
||||
import {
|
||||
MarketStateMapping,
|
||||
MarketTradingModeMapping,
|
||||
@@ -17,11 +16,7 @@ import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const MarketDetails = ({
|
||||
market,
|
||||
}: {
|
||||
market: MarketInfoNoCandlesQuery['market'];
|
||||
}) => {
|
||||
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
@@ -32,7 +27,9 @@ export const MarketDetails = ({
|
||||
if (!market) return null;
|
||||
|
||||
const keyDetails = {
|
||||
...pick(market, 'decimalPlaces', 'positionDecimalPlaces', 'tradingMode'),
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
tradingMode: market.tradingMode,
|
||||
state: MarketStateMapping[market.state],
|
||||
};
|
||||
const assetDecimals =
|
||||
|
||||
@@ -12,6 +12,7 @@ export const Proposals = () => {
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: proposalsDataProvider,
|
||||
variables: {},
|
||||
});
|
||||
|
||||
useDocumentTitle([t('Governance Proposals')]);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { MarketDetails } from '../../components/markets/market-details';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import compact from 'lodash/compact';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { marketInfoNoCandlesDataProvider } from '@vegaprotocol/market-info';
|
||||
import { marketInfoProvider } from '@vegaprotocol/market-info';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
|
||||
export const MarketPage = () => {
|
||||
@@ -16,24 +16,17 @@ export const MarketPage = () => {
|
||||
|
||||
const { marketId } = useParams<{ marketId: string }>();
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId,
|
||||
}),
|
||||
[marketId]
|
||||
);
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketInfoNoCandlesDataProvider,
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
variables,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
skip: !marketId,
|
||||
},
|
||||
});
|
||||
|
||||
useDocumentTitle(
|
||||
compact([
|
||||
'Market details',
|
||||
data?.market?.tradableInstrument.instrument.name,
|
||||
])
|
||||
compact(['Market details', data?.tradableInstrument.instrument.name])
|
||||
);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
@@ -43,10 +36,10 @@ export const MarketPage = () => {
|
||||
<section className="relative">
|
||||
<PageTitle
|
||||
data-testid="markets-heading"
|
||||
title={data?.market?.tradableInstrument.instrument.name || ''}
|
||||
title={data?.tradableInstrument.instrument.name || ''}
|
||||
actions={
|
||||
<Button
|
||||
disabled={!data?.market}
|
||||
disabled={!data}
|
||||
size="xs"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
@@ -60,14 +53,14 @@ export const MarketPage = () => {
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<MarketDetails market={data?.market} />
|
||||
{data && <MarketDetails market={data} />}
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
<JsonViewerDialog
|
||||
open={dialogOpen}
|
||||
onChange={(isOpen) => setDialogOpen(isOpen)}
|
||||
title={data?.market?.tradableInstrument.instrument.name || ''}
|
||||
content={data?.market}
|
||||
title={data?.tradableInstrument.instrument.name || ''}
|
||||
content={data}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const MarketsPage = () => {
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketsProvider,
|
||||
variables: undefined,
|
||||
skipUpdates: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
export const trancheData = {
|
||||
'0': {
|
||||
tranche_id: 0,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'1': {
|
||||
tranche_id: 1,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 1670422330,
|
||||
duration: 1670453883,
|
||||
},
|
||||
'107': {
|
||||
tranche_id: 107,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'110': {
|
||||
tranche_id: 110,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'154': {
|
||||
tranche_id: 154,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'2': {
|
||||
tranche_id: 2,
|
||||
users: [
|
||||
'0x8716c75bb3abe54b959adc529e6355f0422454ed',
|
||||
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
|
||||
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
|
||||
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
|
||||
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
|
||||
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
|
||||
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
|
||||
'0xf73e7ad8aa300a7f86bcc4d55e6c4ea25546e057',
|
||||
],
|
||||
initial_balance: 111300000000000000000,
|
||||
current_balance: 111297025049610000000,
|
||||
cliff_start: 1677578461,
|
||||
duration: 15209600,
|
||||
},
|
||||
'3': {
|
||||
tranche_id: 3,
|
||||
users: [
|
||||
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
|
||||
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
|
||||
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
|
||||
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
|
||||
'0x77a0b7e247b7b8ec99e068a24c401783d6c4dfff',
|
||||
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
|
||||
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
|
||||
],
|
||||
initial_balance: 21000000000000000000,
|
||||
current_balance: 21000000000000000000,
|
||||
cliff_start: 1677578461,
|
||||
duration: 18628800,
|
||||
},
|
||||
'66': {
|
||||
tranche_id: 66,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { trancheData } from '../../fixtures/mocks/tranches';
|
||||
|
||||
const tranches = trancheData;
|
||||
|
||||
context(
|
||||
'Tranches page - verify elements on the page',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
before('visit homepage', function () {
|
||||
cy.intercept('GET', '**/tranches/stats', { tranches });
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('Able to navigate to tranches page', function () {
|
||||
cy.navigate_to('supply');
|
||||
cy.url().should('include', '/token/tranches');
|
||||
cy.get('h1').should('contain.text', 'Vesting tranches');
|
||||
});
|
||||
|
||||
// 1005-VEST-001
|
||||
// 1005-VEST-002
|
||||
it('Able to view tranches', function () {
|
||||
cy.getByTestId('tranche-item')
|
||||
.should('have.length', 2)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('a')
|
||||
.should('have.text', 'Tranche 2') // 1005-VEST-003
|
||||
.and('have.attr', 'href', '/token/tranches/2');
|
||||
cy.get('span').eq(1).should('have.text', '111.30'); // 1005-VEST-005
|
||||
cy.contains('Unlocking starts') // 1005-VEST-008
|
||||
.parent()
|
||||
.should('contain.text', '28 Feb 2023');
|
||||
cy.contains('Fully unlocked')
|
||||
.parent()
|
||||
.should('contain.text', '23 Aug 2023');
|
||||
cy.getByTestId('progress-bar').should('exist');
|
||||
cy.getByTestId('currency-locked') // 1005-VEST-006
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('currency-unlocked') // 1005-VEST-007
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to see individual tranche data', function () {
|
||||
cy.get('[href="/token/tranches/2"]').click();
|
||||
cy.getByTestId('redeemed-tranche-tokens').within(() => {
|
||||
cy.get('span').eq(1).should('have.text', 0);
|
||||
});
|
||||
cy.getByTestId('key-value-table').within(() => {
|
||||
cy.getByTestId('link')
|
||||
.should('have.length', 8)
|
||||
.each((ethLink) => {
|
||||
cy.wrap(ethLink)
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/address/');
|
||||
});
|
||||
cy.getByTestId('redeem-link')
|
||||
.should('have.length', 8)
|
||||
.each((redeemLink) => {
|
||||
cy.wrap(redeemLink)
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', '/token/redeem/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to view tranches with less than 10 vega', function () {
|
||||
cy.navigate_to('supply');
|
||||
cy.getByTestId('show-all-tranches').click();
|
||||
cy.getByTestId('tranche-item')
|
||||
.should('have.length', 8)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('a')
|
||||
.should('have.text', 'Tranche 0')
|
||||
.and('have.attr', 'href', '/token/tranches/0');
|
||||
cy.get('span').eq(1).should('have.text', '0.00');
|
||||
cy.contains('Unlocking starts')
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.contains('Fully unlocked')
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.getByTestId('progress-bar').should('exist');
|
||||
cy.getByTestId('currency-locked')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('currency-unlocked')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,13 +1,15 @@
|
||||
const connectButton = '[data-testid="connect-to-eth-btn"]';
|
||||
const lockedTokensInVestingContract = '6,499,972.30';
|
||||
|
||||
context(
|
||||
'Vesting Page - verify elements on page',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
before('navigate to vesting page', function () {
|
||||
cy.visit('/').navigate_to('vesting');
|
||||
});
|
||||
|
||||
describe('with wallets disconnected', function () {
|
||||
before('navigate to vesting page', function () {
|
||||
cy.visit('/').navigate_to('vesting');
|
||||
});
|
||||
it('should have vesting tab highlighted', function () {
|
||||
cy.verify_tab_highlighted('token');
|
||||
});
|
||||
@@ -16,10 +18,7 @@ context(
|
||||
cy.verify_page_header('Vesting');
|
||||
});
|
||||
|
||||
it('should have connect Eth wallet info', function () {
|
||||
cy.get(connectButton).should('be.visible');
|
||||
});
|
||||
|
||||
// 1005-VEST-018
|
||||
it('should have connect Eth wallet button', function () {
|
||||
cy.get(connectButton)
|
||||
.should('be.visible')
|
||||
@@ -27,18 +26,101 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
describe('with eth wallet connected', function () {
|
||||
describe('With Eth wallet connected', function () {
|
||||
before('connect eth wallet', function () {
|
||||
cy.ethereum_wallet_connect();
|
||||
cy.visit('/');
|
||||
cy.getByTestId('view-connected-eth-btn').click();
|
||||
});
|
||||
|
||||
// 1005-VEST-001
|
||||
// 1005-VEST-002
|
||||
it('Able to view tranches', function () {
|
||||
cy.navigate_to('supply');
|
||||
cy.url().should('include', '/token/tranches');
|
||||
cy.get('h1').should('contain.text', 'Vesting tranches');
|
||||
// 1005-VEST-020 1005-VEST-021
|
||||
it('Tokens in vesting contract for eth wallet is displayed on wallet window', function () {
|
||||
cy.getByTestId('vega-in-vesting-contract').within(() => {
|
||||
cy.getByTestId('currency-title')
|
||||
.should('contain.text', 'VEGA')
|
||||
.and('contain.text', 'In vesting contract');
|
||||
cy.getByTestId('currency-value').should(
|
||||
'have.text',
|
||||
lockedTokensInVestingContract
|
||||
);
|
||||
cy.getByTestId('currency-locked').should(
|
||||
'have.text',
|
||||
lockedTokensInVestingContract
|
||||
);
|
||||
cy.getByTestId('currency-unlocked').should('have.text', '0.00');
|
||||
});
|
||||
});
|
||||
// 1005-VEST-022 1005-VEST-023
|
||||
it('Tokens amount displayed in vesting page', function () {
|
||||
cy.getByTestId(
|
||||
'redemption-description',
|
||||
Cypress.env('txTimeout')
|
||||
).should('exist');
|
||||
const redemptionText =
|
||||
'The connected Ethereum wallet (0xEe7D…d94F) has 6,499,972.30 $VEGA tokens in 1 tranche(s) of the vesting contract.';
|
||||
|
||||
cy.getByTestId('redemption-description').should(
|
||||
'have.text',
|
||||
redemptionText
|
||||
);
|
||||
cy.getByTestId('vesting-table').within(() => {
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Vesting VEGA');
|
||||
cy.get('dd').should('have.text', lockedTokensInVestingContract);
|
||||
});
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Locked');
|
||||
cy.get('dd').should('have.text', lockedTokensInVestingContract);
|
||||
});
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Unlocked');
|
||||
cy.get('dd').should('have.text', '0.00');
|
||||
});
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Associated');
|
||||
cy.get('dd').should('have.text', '0.00');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 1005-VEST-024 1005-VEST-025 1005-VEST-026 1005-VEST-036 1005-VEST-037
|
||||
it('Tokens locked in individual tranches are displayed', function () {
|
||||
cy.getByTestId('tranche-table-footer').should(
|
||||
'have.text',
|
||||
'All the tokens in this tranche are locked and must be assigned to a tranche before they can be redeemed.'
|
||||
);
|
||||
cy.getByTestId('tranche-item').within(() => {
|
||||
cy.get('a')
|
||||
.should('have.text', 'Tranche 0')
|
||||
.and('have.attr', 'href', '/token/tranches/0');
|
||||
cy.get('span')
|
||||
.eq(1)
|
||||
.should('have.text', lockedTokensInVestingContract);
|
||||
cy.contains('Unlocking starts') // 1005-VEST-008
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.contains('Fully unlocked')
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.getByTestId('progress-bar').should('exist');
|
||||
cy.getByTestId('currency-locked') // 1005-VEST-006
|
||||
.should('have.text', lockedTokensInVestingContract);
|
||||
cy.getByTestId('currency-unlocked') // 1005-VEST-007
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('tranche-item-footer').should(
|
||||
'have.text',
|
||||
'All the tokens in this tranche are locked and can not be redeemed yet.'
|
||||
);
|
||||
});
|
||||
cy.connectVegaWallet();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ const RedemptionRouter = () => {
|
||||
fill={true}
|
||||
variant="primary"
|
||||
onClick={() => navigate(`${RoutesConfig.REDEEM}/${account}`)}
|
||||
data-testid="view-connected-eth-btn"
|
||||
>
|
||||
{t('View connected Eth Wallet')}
|
||||
</Button>
|
||||
|
||||
@@ -73,6 +73,7 @@ export const Tranche = () => {
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
data-testid="redeem-link"
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
|
||||
@@ -50,7 +50,10 @@ export const Tranches = () => {
|
||||
)}
|
||||
|
||||
<section className="text-center mt-4">
|
||||
<ButtonLink onClick={() => setShowAll(!showAll)}>
|
||||
<ButtonLink
|
||||
data-testid="show-all-tranches"
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
>
|
||||
{showAll
|
||||
? t(
|
||||
'Showing tranches with <{{trancheMinimum}} VEGA, click to hide these tranches',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMemo } from 'react';
|
||||
import { makeDerivedDataProvider } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
@@ -43,7 +42,7 @@ const useMarketDetails = (marketId: string | undefined) => {
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: lpDataProvider,
|
||||
skipUpdates: true,
|
||||
variables: useMemo(() => ({ marketId }), [marketId]),
|
||||
variables: { marketId: marketId || '' },
|
||||
});
|
||||
|
||||
const liquidityProviders = data?.liquidityProviders || [];
|
||||
|
||||
+7
-10
@@ -39,14 +39,11 @@ export const Last24hVolume = ({
|
||||
[marketId, yTimestamp]
|
||||
);
|
||||
|
||||
const variables24hAgo = useMemo(
|
||||
() => ({
|
||||
marketId: marketId,
|
||||
interval: Schema.Interval.INTERVAL_I1D,
|
||||
since: yTimestamp,
|
||||
}),
|
||||
[marketId, yTimestamp]
|
||||
);
|
||||
const variables24hAgo = {
|
||||
marketId: marketId,
|
||||
interval: Schema.Interval.INTERVAL_I1D,
|
||||
since: yTimestamp,
|
||||
};
|
||||
|
||||
const throttledSetCandles = useRef(
|
||||
throttle((data: Candle[]) => {
|
||||
@@ -64,7 +61,7 @@ export const Last24hVolume = ({
|
||||
[throttledSetCandles]
|
||||
);
|
||||
|
||||
const { data, error } = useDataProvider<Candle[], Candle>({
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: marketCandlesProvider,
|
||||
variables: variables,
|
||||
update,
|
||||
@@ -88,7 +85,7 @@ export const Last24hVolume = ({
|
||||
[throttledSetVolumeChange]
|
||||
);
|
||||
|
||||
useDataProvider<Candle[], Candle>({
|
||||
useDataProvider({
|
||||
dataProvider: marketCandlesProvider,
|
||||
update: updateCandle24hAgo,
|
||||
variables: variables24hAgo,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,7 @@ const completeWithdrawalBtn = 'complete-withdrawal';
|
||||
const submitTransferBtn = '[type="submit"]';
|
||||
const transferForm = 'transfer-form';
|
||||
const depositSubmit = 'deposit-submit';
|
||||
const approveSubmit = 'approve-submit';
|
||||
|
||||
// Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order.
|
||||
describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
@@ -73,13 +74,13 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
cy.getByTestId('approve-warning').should(
|
||||
cy.getByTestId('approve-default').should(
|
||||
'contain.text',
|
||||
`Deposits of ${btcSymbol} not approved`
|
||||
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
|
||||
);
|
||||
cy.getByTestId(depositSubmit).click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.getByTestId(approveSubmit).click();
|
||||
cy.getByTestId('approve-pending').should('exist');
|
||||
cy.getByTestId('approve-confirmed').should('exist');
|
||||
cy.get(amountField).clear().type('10');
|
||||
cy.getByTestId(depositSubmit).click();
|
||||
cy.getByTestId(toastContent, txTimeout).should(
|
||||
|
||||
@@ -31,7 +31,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
it('handles empty fields', () => {
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
||||
// once Ethereum wallet is connected and key selected the only field that will
|
||||
// error is the asset select
|
||||
cy.getByTestId(formFieldError).should('have.length', 1);
|
||||
cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should(
|
||||
'have.length',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it('unable to select assets not enabled', () => {
|
||||
@@ -41,12 +47,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
|
||||
});
|
||||
|
||||
it('invalid public key', () => {
|
||||
cy.get(toAddressField)
|
||||
.clear()
|
||||
.type('INVALID_DEPOSIT_TO_ADDRESS')
|
||||
.next(`[data-testid="${formFieldError}"]`)
|
||||
.should('have.text', 'Invalid Vega key');
|
||||
it('invalid public key when entering address manually', () => {
|
||||
cy.getByTestId('enter-pubkey-manually').click();
|
||||
cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS');
|
||||
cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should(
|
||||
'have.text',
|
||||
'Invalid Vega key'
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid amount', () => {
|
||||
|
||||
@@ -26,20 +26,20 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
|
||||
it('market price', () => {
|
||||
cy.getByTestId(marketTitle).contains('Market price').click();
|
||||
validateMarketDataRow(0, 'Mark Price', '0.05749');
|
||||
validateMarketDataRow(1, 'Best Bid Price', '6.81765 ');
|
||||
validateMarketDataRow(2, 'Best Offer Price', '6.81769 ');
|
||||
validateMarketDataRow(0, 'Mark Price', '46,126.90058');
|
||||
validateMarketDataRow(1, 'Best Bid Price', '44,126.90058 ');
|
||||
validateMarketDataRow(2, 'Best Offer Price', '48,126.90058 ');
|
||||
validateMarketDataRow(3, 'Quote Unit', 'BTC');
|
||||
});
|
||||
|
||||
it('market volume displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Market volume').click();
|
||||
validateMarketDataRow(0, '24 Hour Volume', '-');
|
||||
validateMarketDataRow(0, '24 Hour Volume', '1');
|
||||
validateMarketDataRow(1, 'Open Interest', '0');
|
||||
validateMarketDataRow(2, 'Best Bid Volume', '5');
|
||||
validateMarketDataRow(3, 'Best Offer Volume', '1');
|
||||
validateMarketDataRow(4, 'Best Static Bid Volume', '5');
|
||||
validateMarketDataRow(5, 'Best Static Offer Volume', '1');
|
||||
validateMarketDataRow(2, 'Best Bid Volume', '1');
|
||||
validateMarketDataRow(3, 'Best Offer Volume', '3');
|
||||
validateMarketDataRow(4, 'Best Static Bid Volume', '2');
|
||||
validateMarketDataRow(5, 'Best Static Offer Volume', '4');
|
||||
});
|
||||
|
||||
it('insurance pool displayed', () => {
|
||||
@@ -149,9 +149,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
.contains(/Liquidity(?! m)/)
|
||||
.click();
|
||||
|
||||
validateMarketDataRow(0, 'Target Stake', '0.56789 tBTC');
|
||||
validateMarketDataRow(1, 'Supplied Stake', '0.56767 tBTC');
|
||||
validateMarketDataRow(2, 'Market Value Proxy', '6.77678 tBTC');
|
||||
validateMarketDataRow(0, 'Target Stake', '10.00 tBTC');
|
||||
validateMarketDataRow(1, 'Supplied Stake', '0.01 tBTC');
|
||||
validateMarketDataRow(2, 'Market Value Proxy', '20.00 tBTC');
|
||||
|
||||
cy.getByTestId('view-liquidity-link').should(
|
||||
'have.text',
|
||||
@@ -163,8 +163,8 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
|
||||
|
||||
validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
|
||||
validateMarketDataRow(1, 'Lowest Price', '0.05634 BTC');
|
||||
validateMarketDataRow(2, 'Highest Price', '0.05864 BTC');
|
||||
validateMarketDataRow(1, 'Lowest Price', '45,204.362 BTC');
|
||||
validateMarketDataRow(2, 'Highest Price', '47,049.438 BTC');
|
||||
});
|
||||
|
||||
it('oracle displayed', () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="deposited"]')
|
||||
.should('have.text', '1,001.00');
|
||||
.should('have.text', '100,001.01');
|
||||
});
|
||||
describe('sorting by ag-grid columns should work well', () => {
|
||||
it('sorting by asset', () => {
|
||||
@@ -58,24 +58,24 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
const marketsSortedDefault = [
|
||||
'1,000.00002',
|
||||
'1,001.00',
|
||||
'1,000.01',
|
||||
'100,001.01',
|
||||
'1,000.01',
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
'1,000.00002',
|
||||
'1,000.01',
|
||||
'1,000.01',
|
||||
'1,001.00',
|
||||
'100,001.01',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'1,001.00',
|
||||
'1,000.01',
|
||||
'100,001.01',
|
||||
'1,000.01',
|
||||
'1,000.00002',
|
||||
'1,000.00001',
|
||||
'1,000.00',
|
||||
];
|
||||
checkSorting(
|
||||
'deposited',
|
||||
@@ -87,9 +87,9 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
|
||||
it('sorting by used', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
const marketsSortedDefault = ['0.00', '1.00', '0.01', '0.01', '0.00'];
|
||||
const marketsSortedAsc = ['0.00', '0.00', '0.01', '0.01', '1.00'];
|
||||
const marketsSortedDesc = ['1.00', '0.01', '0.01', '0.00', '0.00'];
|
||||
const marketsSortedDefault = ['0.00', '1.01', '0.01', '0.00', '0.00'];
|
||||
const marketsSortedAsc = ['0.00', '0.00', '0.00', '0.01', '1.01'];
|
||||
const marketsSortedDesc = ['1.01', '0.01', '0.00', '0.00', '0.00'];
|
||||
checkSorting(
|
||||
'used',
|
||||
marketsSortedDefault,
|
||||
@@ -102,24 +102,24 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
const marketsSortedDefault = [
|
||||
'1,000.00002',
|
||||
'1,000.00',
|
||||
'100,000.00',
|
||||
'1,000.00',
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'1,000.00',
|
||||
'1,000.00',
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
'1,000.00002',
|
||||
'100,000.00',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'100,000.00',
|
||||
'1,000.00002',
|
||||
'1,000.00001',
|
||||
'1,000.00',
|
||||
'1,000.00',
|
||||
'1,000.00',
|
||||
];
|
||||
|
||||
checkSorting(
|
||||
|
||||
@@ -2,7 +2,11 @@ import * as Schema from '@vegaprotocol/types';
|
||||
import { aliasGQLQuery, mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
import { testOrderSubmission } from '../support/order-validation';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { accountsQuery, estimateOrderQuery } from '@vegaprotocol/mock';
|
||||
import {
|
||||
accountsQuery,
|
||||
estimateOrderQuery,
|
||||
amendGeneralAccountBalance,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { createOrder } from '../support/create-order';
|
||||
|
||||
const orderSizeField = 'order-size';
|
||||
@@ -583,6 +587,10 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
const accounts = accountsQuery();
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Accounts', accounts);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
@@ -632,30 +640,10 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
const accounts = accountsQuery();
|
||||
amendGeneralAccountBalance(accounts, 'market-0', '0');
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'Accounts',
|
||||
accountsQuery({
|
||||
party: {
|
||||
accountsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '0',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
aliasGQLQuery(req, 'Accounts', accounts);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
@@ -669,7 +657,7 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
'have.text',
|
||||
'You need ' +
|
||||
'tDAI' +
|
||||
' in your wallet to trade in this market. See all your collateral.Make a deposit'
|
||||
' in your wallet to trade in this market.See all your collateral.Make a deposit'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
||||
});
|
||||
@@ -679,19 +667,13 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
const accounts = accountsQuery();
|
||||
amendGeneralAccountBalance(accounts, 'market-0', '100000000');
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'EstimateOrder',
|
||||
estimateOrderQuery({
|
||||
estimateOrder: {
|
||||
marginLevels: {
|
||||
__typename: 'MarginLevels',
|
||||
initialLevel: '1000000000',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
aliasGQLQuery(req, 'Accounts', accounts);
|
||||
});
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
@@ -707,7 +689,7 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
);
|
||||
cy.getByTestId('dealticket-warning-margin').should(
|
||||
'contain.text',
|
||||
'9,999.99 tDAI is currently required. You have only 1,000.00 tDAI available.Deposit tDAI'
|
||||
'You may not have enough margin available to open this position. 2,354.72283 tDAI is currently required. You have only 1,000.01 tDAI available.'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
||||
cy.getByTestId('dialog-content')
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('orders list', { tags: '@smoke' }, () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('Orders').click();
|
||||
cy.wait('@Orders').then(() => {
|
||||
expect(subscriptionMocks.OrdersUpdate).to.be.calledTwice;
|
||||
expect(subscriptionMocks.OrdersUpdate).to.be.calledThrice;
|
||||
});
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
@@ -136,7 +136,7 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('Orders').click();
|
||||
cy.wait('@Orders').then(() => {
|
||||
expect(subscriptionMocks.OrdersUpdate).to.be.calledTwice;
|
||||
expect(subscriptionMocks.OrdersUpdate).to.be.calledThrice;
|
||||
});
|
||||
});
|
||||
const orderId = '1234567890';
|
||||
@@ -354,7 +354,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('Orders').click();
|
||||
cy.wait('@Orders').then(() => {
|
||||
expect(subscriptionMocks.OrdersUpdate).to.be.calledTwice;
|
||||
expect(subscriptionMocks.OrdersUpdate).to.be.calledThrice;
|
||||
});
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
|
||||
@@ -164,10 +164,10 @@ describe('positions', { tags: '@smoke' }, () => {
|
||||
|
||||
cy.get('[col-id="liquidationPrice"]').should('contain.text', '0'); // liquidation price
|
||||
|
||||
cy.get('[col-id="currentLeverage"]').should('contain.text', '138.446.1');
|
||||
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
|
||||
|
||||
cy.get('[col-id="marginAccountBalance"]') // margin allocated
|
||||
.should('contain.text', '1,000');
|
||||
.should('contain.text', '0.01');
|
||||
|
||||
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
|
||||
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
|
||||
|
||||
@@ -17,6 +17,13 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-003
|
||||
// 0002-WCON-039
|
||||
// 0002-WCON-017
|
||||
// 0002-WCON-018
|
||||
// 0002-WCON-019
|
||||
|
||||
// Mock authentication
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
@@ -41,6 +48,9 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
},
|
||||
});
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.contains(
|
||||
'Choose wallet app to connect, or to change port or server URL enter a custom wallet location first'
|
||||
);
|
||||
cy.contains('Connect Vega wallet');
|
||||
cy.contains('Hosted Fairground wallet');
|
||||
|
||||
@@ -51,9 +61,13 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(form).find('#passphrase').click().type('pass');
|
||||
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
cy.getByTestId('manage-vega-wallet').click();
|
||||
cy.getByTestId('keypair-list').should('exist');
|
||||
});
|
||||
|
||||
it('doesnt connect with invalid credentials', () => {
|
||||
// 0002-WCON-020
|
||||
|
||||
// Mock incorrect username/password
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
@@ -99,6 +113,10 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
|
||||
mockConnectWallet();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
@@ -110,16 +128,40 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('can change selected public key and disconnect', () => {
|
||||
// 0002-WCON-022
|
||||
// 0002-WCON-023
|
||||
// 0002-WCON-025
|
||||
// 0002-WCON-026
|
||||
// 0002-WCON-021
|
||||
// 0002-WCON-027
|
||||
// 0002-WCON-030
|
||||
// 0002-WCON-029
|
||||
// 0002-WCON-008
|
||||
// 0002-WCON-035
|
||||
// 0002-WCON-014
|
||||
// 0002-WCON-010
|
||||
|
||||
mockConnectWallet();
|
||||
const key2 = Cypress.env('VEGA_PUBLIC_KEY2');
|
||||
const truncatedKey2 = Cypress.env('TRUNCATED_VEGA_PUBLIC_KEY2');
|
||||
cy.connectVegaWallet();
|
||||
cy.getByTestId('manage-vega-wallet').click();
|
||||
cy.getByTestId('keypair-list').should('exist');
|
||||
cy.getByTestId(`key-${key2}`).should('contain.text', truncatedKey2);
|
||||
cy.getByTestId(`key-${key2}`)
|
||||
.find('[data-testid="copy-vega-public-key"]')
|
||||
.should('be.visible');
|
||||
cy.get(`[data-testid="key-${key2}"] > .mr-2`).click();
|
||||
cy.getByTestId('keypair-list')
|
||||
.find('[data-state="checked"]')
|
||||
.should('be.visible');
|
||||
cy.getByTestId('disconnect').click();
|
||||
cy.getByTestId('connect-vega-wallet').should('exist');
|
||||
cy.getByTestId('manage-vega-wallet').should('not.exist');
|
||||
cy.getByTestId('connect-vega-wallet').click();
|
||||
cy.contains(
|
||||
'Choose wallet app to connect, or to change port or server URL enter a custom wallet location first'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,7 +193,7 @@ describe('ethereum wallet', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.get('#ethereum-address').should('have.value', ethWalletAddress);
|
||||
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
|
||||
cy.getByTestId('disconnect-ethereum-wallet')
|
||||
.should('have.text', 'Disconnect')
|
||||
.click();
|
||||
|
||||
@@ -76,6 +76,9 @@ describe(
|
||||
cy.getByTestId(closeDialog).click();
|
||||
cy.getByTestId('Trading').first().click();
|
||||
cy.getByTestId(collateralTab).click();
|
||||
cy.getByTestId(openTransferDialog).should('not.exist');
|
||||
cy.getByTestId('Portfolio').eq(0).click();
|
||||
cy.getByTestId(collateralTab).click();
|
||||
cy.getByTestId(openTransferDialog).click();
|
||||
cy.getByTestId(dialogTransferText).should(
|
||||
'contain.text',
|
||||
|
||||
@@ -7,13 +7,13 @@ import type { onMessage } from '@vegaprotocol/cypress';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import { orderUpdateSubscription } from '@vegaprotocol/mock';
|
||||
|
||||
let sendOrderUpdate: (data: OrdersUpdateSubscription) => void;
|
||||
const sendOrderUpdate: ((data: OrdersUpdateSubscription) => void)[] = [];
|
||||
const getOnOrderUpdate = () => {
|
||||
const onOrderUpdate: onMessage<
|
||||
OrdersUpdateSubscription,
|
||||
OrdersUpdateSubscriptionVariables
|
||||
> = (send) => {
|
||||
sendOrderUpdate = send;
|
||||
sendOrderUpdate.push(send);
|
||||
};
|
||||
return onOrderUpdate;
|
||||
};
|
||||
@@ -31,5 +31,5 @@ export function updateOrder(
|
||||
if (!sendOrderUpdate) {
|
||||
throw new Error('OrderSub not called');
|
||||
}
|
||||
sendOrderUpdate(update);
|
||||
sendOrderUpdate.forEach((send) => send(update));
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from '@vegaprotocol/mock';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/market-list';
|
||||
import type { MarketInfoQuery } from '@vegaprotocol/market-info';
|
||||
|
||||
type MarketPageMockData = {
|
||||
state: Schema.MarketState;
|
||||
@@ -69,18 +68,6 @@ const marketsDataOverride = (
|
||||
},
|
||||
});
|
||||
|
||||
const marketInfoOverride = (
|
||||
data: MarketPageMockData
|
||||
): PartialDeep<MarketInfoQuery> => ({
|
||||
market: {
|
||||
state: data.state,
|
||||
tradingMode: data.tradingMode,
|
||||
data: {
|
||||
trigger: data.trigger,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockTradingPage = (
|
||||
req: CyHttpMessages.IncomingHttpRequest,
|
||||
state: Schema.MarketState = Schema.MarketState.STATE_ACTIVE,
|
||||
@@ -109,11 +96,7 @@ const mockTradingPage = (
|
||||
aliasGQLQuery(req, 'Margins', marginsQuery());
|
||||
aliasGQLQuery(req, 'Assets', assetsQuery());
|
||||
aliasGQLQuery(req, 'Asset', assetQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'MarketInfo',
|
||||
marketInfoQuery(marketInfoOverride({ state, tradingMode, trigger }))
|
||||
);
|
||||
aliasGQLQuery(req, 'MarketInfo', marketInfoQuery());
|
||||
aliasGQLQuery(req, 'Trades', tradesQuery());
|
||||
aliasGQLQuery(req, 'Chart', chartQuery());
|
||||
aliasGQLQuery(req, 'Candles', candlesQuery());
|
||||
|
||||
@@ -12,6 +12,7 @@ export const Home = () => {
|
||||
// should be the oldest market that is currently trading in us mode(i.e. not in auction).
|
||||
const { data, error, loading } = useDataProvider({
|
||||
dataProvider: marketsWithDataProvider,
|
||||
variables: undefined,
|
||||
});
|
||||
const update = useGlobalStore((store) => store.update);
|
||||
const marketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
@@ -47,7 +47,8 @@ export const Liquidity = () => {
|
||||
const useReloadLiquidityData = (marketId: string | undefined) => {
|
||||
const { reload } = useDataProvider({
|
||||
dataProvider: liquidityProvisionsDataProvider,
|
||||
variables: useMemo(() => ({ marketId }), [marketId]),
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !marketId,
|
||||
});
|
||||
useEffect(() => {
|
||||
const interval = setInterval(reload, 10000);
|
||||
@@ -77,7 +78,8 @@ export const LiquidityContainer = ({
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: lpAggregatedDataProvider,
|
||||
update,
|
||||
variables: useMemo(() => ({ marketId }), [marketId]),
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const assetDecimalPlaces =
|
||||
@@ -161,7 +163,8 @@ export const LiquidityViewContainer = ({
|
||||
} = useDataProvider({
|
||||
dataProvider: lpAggregatedDataProvider,
|
||||
update,
|
||||
variables: useMemo(() => ({ marketId }), [marketId]),
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const targetStake = marketData?.targetStake;
|
||||
|
||||
@@ -7,10 +7,6 @@ import {
|
||||
useThrottledDataProvider,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
MarketData,
|
||||
MarketDataUpdateFieldsFragment,
|
||||
} from '@vegaprotocol/market-list';
|
||||
|
||||
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
|
||||
import { useGlobalStore, usePageTitleStore } from '../../stores';
|
||||
@@ -35,13 +31,10 @@ const TitleUpdater = ({
|
||||
}) => {
|
||||
const pageTitle = usePageTitleStore((store) => store.pageTitle);
|
||||
const updateTitle = usePageTitleStore((store) => store.updateTitle);
|
||||
const { data: marketData } = useThrottledDataProvider<
|
||||
MarketData,
|
||||
MarketDataUpdateFieldsFragment
|
||||
>(
|
||||
const { data: marketData } = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketDataProvider,
|
||||
variables: useMemo(() => ({ marketId }), [marketId]),
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !marketId,
|
||||
},
|
||||
1000
|
||||
|
||||
@@ -179,7 +179,10 @@ const MainGrid = ({
|
||||
</Tab>
|
||||
<Tab id="accounts" name={t('Collateral')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Collateral pinnedAsset={pinnedAsset} />
|
||||
<TradingViews.Collateral
|
||||
pinnedAsset={pinnedAsset}
|
||||
hideButtons
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -2,10 +2,16 @@ import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useDepositDialog, DepositsTable } from '@vegaprotocol/deposits';
|
||||
import { depositsProvider } from '@vegaprotocol/deposits';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useDataProvider,
|
||||
useBottomPlaceholder,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useRef } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
|
||||
export const DepositsContainer = () => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { data, loading, error, reload } = useDataProvider({
|
||||
dataProvider: depositsProvider,
|
||||
@@ -13,13 +19,15 @@ export const DepositsContainer = () => {
|
||||
skip: !pubKey,
|
||||
});
|
||||
const openDepositDialog = useDepositDialog((state) => state.open);
|
||||
|
||||
const bottomPlaceholderProps = useBottomPlaceholder({ gridRef });
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[1fr,min-content]">
|
||||
<div className="h-full">
|
||||
<div className="h-full relative">
|
||||
<DepositsTable
|
||||
rowData={data || []}
|
||||
noRowsOverlayComponent={() => null}
|
||||
ref={gridRef}
|
||||
{...bottomPlaceholderProps}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
@@ -33,8 +41,9 @@ export const DepositsContainer = () => {
|
||||
</div>
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
|
||||
<div className="h-auto flex justify-end px-[11px] py-2 bottom-0 right-3 absolute dark:bg-black/75 bg-white/75 rounded">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => openDepositDialog()}
|
||||
data-testid="deposit-button"
|
||||
|
||||
@@ -49,7 +49,10 @@ export const Portfolio = () => {
|
||||
</Tab>
|
||||
<Tab id="positions" name={t('Positions')}>
|
||||
<VegaWalletContainer>
|
||||
<PositionsContainer onMarketClick={onMarketClick} />
|
||||
<PositionsContainer
|
||||
onMarketClick={onMarketClick}
|
||||
noBottomPlaceholder
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
|
||||
@@ -20,36 +20,35 @@ export const WithdrawalsContainer = () => {
|
||||
|
||||
return (
|
||||
<VegaWalletContainer>
|
||||
<div className="h-full relative grid grid-rows-[1fr,min-content]">
|
||||
<div className="h-full relative">
|
||||
<WithdrawalsTable
|
||||
data-testid="withdrawals-history"
|
||||
rowData={data}
|
||||
noRowsOverlayComponent={() => null}
|
||||
<div className="h-full relative">
|
||||
<WithdrawalsTable
|
||||
data-testid="withdrawals-history"
|
||||
rowData={data}
|
||||
noRowsOverlayComponent={() => null}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
noDataMessage={t('No withdrawals')}
|
||||
reload={reload}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
noDataMessage={t('No withdrawals')}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openWithdrawDialog()}
|
||||
data-testid="withdraw-dialog-button"
|
||||
>
|
||||
{t('Make withdrawal')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
<div className="h-auto flex justify-end px-[11px] py-2 bottom-0 right-3 absolute dark:bg-black/75 bg-white/75 rounded">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => openWithdrawDialog()}
|
||||
data-testid="withdraw-dialog-button"
|
||||
>
|
||||
{t('Make withdrawal')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,8 +11,10 @@ import { useDepositDialog } from '@vegaprotocol/deposits';
|
||||
|
||||
export const AccountsContainer = ({
|
||||
pinnedAsset,
|
||||
hideButtons,
|
||||
}: {
|
||||
pinnedAsset?: PinnedAsset;
|
||||
hideButtons?: boolean;
|
||||
}) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
@@ -36,27 +38,30 @@ export const AccountsContainer = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full relative grid grid-rows-[1fr,min-content]">
|
||||
<div>
|
||||
<AccountManager
|
||||
partyId={pubKey}
|
||||
onClickAsset={onClickAsset}
|
||||
onClickWithdraw={openWithdrawalDialog}
|
||||
onClickDeposit={openDepositDialog}
|
||||
isReadOnly={isReadOnly}
|
||||
pinnedAsset={pinnedAsset}
|
||||
/>
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
<div className="flex gap-2 justify-end p-2 px-[11px]">
|
||||
<div className="h-full relative">
|
||||
<AccountManager
|
||||
partyId={pubKey}
|
||||
onClickAsset={onClickAsset}
|
||||
onClickWithdraw={openWithdrawalDialog}
|
||||
onClickDeposit={openDepositDialog}
|
||||
isReadOnly={isReadOnly}
|
||||
pinnedAsset={pinnedAsset}
|
||||
/>
|
||||
{!isReadOnly && !hideButtons && (
|
||||
<div className="flex gap-2 justify-end p-2 px-[11px] absolute lg:fixed bottom-0 right-3 dark:bg-black/75 bg-white/75 rounded">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
data-testid="open-transfer-dialog"
|
||||
onClick={() => openTransferDialog()}
|
||||
>
|
||||
{t('Transfer')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => openDepositDialog()}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => openDepositDialog()}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Footer = () => {
|
||||
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
|
||||
|
||||
return (
|
||||
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300">
|
||||
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300 lg:fixed bottom-0 left-0 border-r bg-white dark:bg-black">
|
||||
{/* Pull left to align with top nav, due to button padding */}
|
||||
<div className="-ml-2">
|
||||
{VEGA_URL && (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { isNumeric } from '@vegaprotocol/utils';
|
||||
import {
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
import { PriceChangeCell } from '@vegaprotocol/datagrid';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { CandleClose } from '@vegaprotocol/types';
|
||||
import type { Candle } from '@vegaprotocol/market-list';
|
||||
import { marketCandlesProvider } from '@vegaprotocol/market-list';
|
||||
import { THROTTLE_UPDATE_TIME } from '../constants';
|
||||
|
||||
@@ -30,19 +28,14 @@ export const Last24hPriceChange = ({
|
||||
}: Props) => {
|
||||
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
||||
const yesterday = useYesterday();
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId: marketId,
|
||||
interval: Schema.Interval.INTERVAL_I1H,
|
||||
since: new Date(yesterday).toISOString(),
|
||||
}),
|
||||
[marketId, yesterday]
|
||||
);
|
||||
|
||||
const { data, error } = useThrottledDataProvider<Candle[], Candle>(
|
||||
const { data, error } = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketCandlesProvider,
|
||||
variables,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
interval: Schema.Interval.INTERVAL_I1H,
|
||||
since: new Date(yesterday).toISOString(),
|
||||
},
|
||||
skip: !marketId || !inView,
|
||||
},
|
||||
THROTTLE_UPDATE_TIME
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
useYesterday,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useMemo } from 'react';
|
||||
import type { Candle } from '@vegaprotocol/market-list';
|
||||
import { THROTTLE_UPDATE_TIME } from '../constants';
|
||||
|
||||
interface Props {
|
||||
@@ -32,19 +30,14 @@ export const Last24hVolume = ({
|
||||
const yesterday = useYesterday();
|
||||
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId: marketId,
|
||||
interval: Schema.Interval.INTERVAL_I1H,
|
||||
since: new Date(yesterday).toISOString(),
|
||||
}),
|
||||
[marketId, yesterday]
|
||||
);
|
||||
|
||||
const { data } = useThrottledDataProvider<Candle[], Candle>(
|
||||
const { data } = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketCandlesProvider,
|
||||
variables,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
interval: Schema.Interval.INTERVAL_I1H,
|
||||
since: new Date(yesterday).toISOString(),
|
||||
},
|
||||
skip: !(inView && marketId),
|
||||
},
|
||||
THROTTLE_UPDATE_TIME
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
useDataProvider,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
MarketData,
|
||||
MarketDataUpdateFieldsFragment,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import type { MarketData } from '@vegaprotocol/market-list';
|
||||
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
|
||||
import { HeaderStat } from '../header';
|
||||
import {
|
||||
@@ -70,7 +67,7 @@ export const MarketLiquiditySupplied = ({
|
||||
[noUpdate]
|
||||
);
|
||||
|
||||
useDataProvider<MarketData, MarketDataUpdateFieldsFragment>({
|
||||
useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
update,
|
||||
variables,
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { PriceCell } from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
MarketData,
|
||||
MarketDataUpdateFieldsFragment,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { marketDataProvider } from '@vegaprotocol/market-list';
|
||||
import { THROTTLE_UPDATE_TIME } from '../constants';
|
||||
|
||||
@@ -27,15 +22,10 @@ export const MarketMarkPrice = ({
|
||||
asPriceCell,
|
||||
}: Props) => {
|
||||
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
||||
const variables = useMemo(() => ({ marketId }), [marketId]);
|
||||
|
||||
const { data } = useThrottledDataProvider<
|
||||
MarketData,
|
||||
MarketDataUpdateFieldsFragment
|
||||
>(
|
||||
const { data } = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketDataProvider,
|
||||
variables,
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !inView,
|
||||
},
|
||||
THROTTLE_UPDATE_TIME
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import type {
|
||||
MarketData,
|
||||
MarketDataUpdateFieldsFragment,
|
||||
Market,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import type { MarketData, Market } from '@vegaprotocol/market-list';
|
||||
import { marketDataProvider } from '@vegaprotocol/market-list';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { HeaderStat } from '../header';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import * as constants from '../constants';
|
||||
|
||||
export const MarketState = ({ market }: { market: Market | null }) => {
|
||||
@@ -33,14 +29,10 @@ export const MarketState = ({ market }: { market: Market | null }) => {
|
||||
[throttledSetMarketState]
|
||||
);
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({ marketId: market?.id || '' }),
|
||||
[market?.id]
|
||||
);
|
||||
useDataProvider<MarketData, MarketDataUpdateFieldsFragment>({
|
||||
useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
update,
|
||||
variables,
|
||||
variables: { marketId: market?.id || '' },
|
||||
skip: !market?.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
MarketData,
|
||||
MarketDataUpdateFieldsFragment,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import type { MarketData } from '@vegaprotocol/market-list';
|
||||
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
|
||||
import { HeaderStat } from '../header';
|
||||
import * as constants from '../constants';
|
||||
|
||||
export const MarketVolume = ({ marketId }: { marketId: string }) => {
|
||||
const [marketVolume, setMarketVolume] = useState<string>('-');
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId: marketId,
|
||||
}),
|
||||
[marketId]
|
||||
);
|
||||
const variables = { marketId };
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketProvider,
|
||||
variables,
|
||||
@@ -46,7 +38,7 @@ export const MarketVolume = ({ marketId }: { marketId: string }) => {
|
||||
[data?.positionDecimalPlaces, throttledSetMarketVolume]
|
||||
);
|
||||
|
||||
useDataProvider<MarketData, MarketDataUpdateFieldsFragment>({
|
||||
useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
update,
|
||||
variables,
|
||||
|
||||
@@ -106,14 +106,13 @@ export const SelectMarketPopover = ({
|
||||
loading: marketsLoading,
|
||||
reload: marketListReload,
|
||||
} = useMarketList();
|
||||
const variables = useMemo(() => ({ partyId: pubKey }), [pubKey]);
|
||||
const {
|
||||
data: positions,
|
||||
loading: positionsLoading,
|
||||
reload,
|
||||
} = useDataProvider({
|
||||
dataProvider: positionsDataProvider,
|
||||
variables,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
const onSelectMarket = useCallback(
|
||||
|
||||
@@ -20,6 +20,7 @@ export const WelcomeDialog = () => {
|
||||
const [riskAccepted] = useLocalStorage(constants.RISK_ACCEPTED_KEY);
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: activeMarketsProvider,
|
||||
variables: undefined,
|
||||
});
|
||||
|
||||
const { update, shouldDisplayWelcomeDialog } = useGlobalStore((store) => ({
|
||||
|
||||
@@ -177,22 +177,14 @@ export const useEthereumTransactionToasts = () => {
|
||||
store.remove,
|
||||
]);
|
||||
|
||||
const [dismissTx, deleteTx] = useEthTransactionStore((state) => [
|
||||
state.dismiss,
|
||||
state.delete,
|
||||
]);
|
||||
const dismissTx = useEthTransactionStore((state) => state.dismiss);
|
||||
|
||||
const onClose = useCallback(
|
||||
(tx: EthStoredTxState) => () => {
|
||||
const safeToDelete = isFinal(tx);
|
||||
if (safeToDelete) {
|
||||
deleteTx(tx.id);
|
||||
} else {
|
||||
dismissTx(tx.id);
|
||||
}
|
||||
dismissTx(tx.id);
|
||||
removeToast(`eth-${tx.id}`);
|
||||
},
|
||||
[deleteTx, dismissTx, removeToast]
|
||||
[dismissTx, removeToast]
|
||||
);
|
||||
|
||||
const fromEthTransaction = useCallback(
|
||||
|
||||
@@ -72,7 +72,7 @@ function AppBody({ Component }: AppProps) {
|
||||
|
||||
const gridClasses = classNames(
|
||||
'h-full relative z-0 grid',
|
||||
'grid-rows-[repeat(3,min-content),1fr,min-content]'
|
||||
'grid-rows-[repeat(3,min-content),1fr]'
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
AccountFieldsFragment,
|
||||
AccountsQuery,
|
||||
AccountEventsSubscription,
|
||||
AccountsQueryVariables,
|
||||
} from './__generated__/Accounts';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
@@ -85,7 +86,8 @@ export const accountsOnlyDataProvider = makeDataProvider<
|
||||
AccountsQuery,
|
||||
AccountFieldsFragment[],
|
||||
AccountEventsSubscription,
|
||||
AccountEventsSubscription['accounts']
|
||||
AccountEventsSubscription['accounts'],
|
||||
AccountsQueryVariables
|
||||
>({
|
||||
query: AccountsDocument,
|
||||
subscriptionQuery: AccountEventsDocument,
|
||||
@@ -159,8 +161,16 @@ const getAssetAccountAggregation = (
|
||||
return { ...balanceAccount, breakdown };
|
||||
};
|
||||
|
||||
export const accountsDataProvider = makeDerivedDataProvider<Account[], never>(
|
||||
[accountsOnlyDataProvider, marketsProvider, assetsProvider],
|
||||
export const accountsDataProvider = makeDerivedDataProvider<
|
||||
Account[],
|
||||
never,
|
||||
AccountsQueryVariables
|
||||
>(
|
||||
[
|
||||
accountsOnlyDataProvider,
|
||||
(callback, client) => marketsProvider(callback, client, undefined),
|
||||
(callback, client) => assetsProvider(callback, client, undefined),
|
||||
],
|
||||
([accounts, markets, assets]): Account[] | null => {
|
||||
return accounts
|
||||
? accounts
|
||||
@@ -194,7 +204,8 @@ export const accountsDataProvider = makeDerivedDataProvider<Account[], never>(
|
||||
|
||||
export const aggregatedAccountsDataProvider = makeDerivedDataProvider<
|
||||
AccountFields[],
|
||||
never
|
||||
never,
|
||||
AccountsQueryVariables
|
||||
>(
|
||||
[accountsDataProvider],
|
||||
(parts) => parts[0] && getAccountData(parts[0] as Account[])
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useRef, useMemo, memo, useCallback } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useDataProvider,
|
||||
useBottomPlaceholder,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef, useMemo, memo } from 'react';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { aggregatedAccountsDataProvider } from './accounts-data-provider';
|
||||
import type { PinnedAsset } from './accounts-table';
|
||||
import { AccountTable } from './accounts-table';
|
||||
import type { RowHeightParams } from 'ag-grid-community';
|
||||
|
||||
interface AccountManagerProps {
|
||||
partyId: string;
|
||||
@@ -27,14 +31,26 @@ export const AccountManager = ({
|
||||
}: AccountManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
|
||||
const { data, loading, error, reload } = useDataProvider<
|
||||
AccountFields[],
|
||||
never
|
||||
>({
|
||||
const { data, loading, error, reload } = useDataProvider({
|
||||
dataProvider: aggregatedAccountsDataProvider,
|
||||
variables,
|
||||
});
|
||||
const setId = useCallback(
|
||||
(data: AccountFields) => ({
|
||||
...data,
|
||||
asset: { ...data.asset, id: `${data.asset.id}-1` },
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
|
||||
gridRef,
|
||||
setId,
|
||||
});
|
||||
|
||||
const getRowHeight = useCallback(
|
||||
(params: RowHeightParams) => (params.node.rowPinned ? 32 : 22),
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<AccountTable
|
||||
@@ -46,6 +62,8 @@ export const AccountManager = ({
|
||||
isReadOnly={isReadOnly}
|
||||
noRowsOverlayComponent={() => null}
|
||||
pinnedAsset={pinnedAsset}
|
||||
getRowHeight={getRowHeight}
|
||||
{...bottomPlaceholderProps}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { forwardRef, useMemo, useState } from 'react';
|
||||
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
isNumeric,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
VegaValueGetterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
AgGridDynamic as AgGrid,
|
||||
CenteredGridCellWrapper,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
@@ -86,139 +94,202 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
node,
|
||||
}: VegaICellRendererParams<AccountFields, 'asset.symbol'>) => {
|
||||
return value ? (
|
||||
<ButtonLink
|
||||
data-testid="asset"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
onClickAsset(data.asset.id);
|
||||
}
|
||||
}}
|
||||
<CenteredGridCellWrapper
|
||||
className={node.rowPinned ? 'h-[30px]' : undefined}
|
||||
>
|
||||
{value}
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
data-testid="asset"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
onClickAsset(data.asset.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</ButtonLink>
|
||||
</CenteredGridCellWrapper>
|
||||
) : null;
|
||||
}}
|
||||
maxWidth={300}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Total')}
|
||||
type="rightAligned"
|
||||
field="deposited"
|
||||
headerTooltip={t(
|
||||
'This is the total amount of collateral used plus the amount available in your general account.'
|
||||
)}
|
||||
valueFormatter={({
|
||||
value,
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'deposited'>) =>
|
||||
data &&
|
||||
data.asset &&
|
||||
isNumeric(value) &&
|
||||
addDecimalsFormatNumber(value, data.asset.decimals)
|
||||
}
|
||||
}: VegaValueGetterParams<AccountFields, 'deposited'>) => {
|
||||
return !data?.deposited
|
||||
? undefined
|
||||
: toBigNum(data.deposited, data.asset.decimals).toNumber();
|
||||
}}
|
||||
maxWidth={300}
|
||||
cellRenderer={({
|
||||
data,
|
||||
node,
|
||||
}: VegaICellRendererParams<AccountFields, 'deposited'>) => {
|
||||
const valueFormatted =
|
||||
data &&
|
||||
data.asset &&
|
||||
isNumeric(data.deposited) &&
|
||||
addDecimalsFormatNumber(data.deposited, data.asset.decimals);
|
||||
return node.rowPinned ? (
|
||||
<CenteredGridCellWrapper className="h-[30px] justify-end">
|
||||
{valueFormatted}
|
||||
</CenteredGridCellWrapper>
|
||||
) : (
|
||||
valueFormatted
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Used')}
|
||||
type="rightAligned"
|
||||
field="used"
|
||||
headerTooltip={t(
|
||||
'This is the amount of collateral used from your general account.'
|
||||
)}
|
||||
valueFormatter={({
|
||||
value,
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'used'>) =>
|
||||
data &&
|
||||
data.asset &&
|
||||
isNumeric(value) &&
|
||||
addDecimalsFormatNumber(value, data.asset.decimals)
|
||||
}
|
||||
}: VegaValueGetterParams<AccountFields, 'used'>) => {
|
||||
return !data?.used
|
||||
? undefined
|
||||
: toBigNum(data.used, data.asset.decimals).toNumber();
|
||||
}}
|
||||
maxWidth={300}
|
||||
cellRenderer={({
|
||||
data,
|
||||
node,
|
||||
}: VegaICellRendererParams<AccountFields, 'used'>) => {
|
||||
const valueFormatted =
|
||||
data &&
|
||||
data.asset &&
|
||||
isNumeric(data.used) &&
|
||||
addDecimalsFormatNumber(data.used, data.asset.decimals);
|
||||
return node.rowPinned ? (
|
||||
<CenteredGridCellWrapper className="h-[30px] justify-end">
|
||||
{valueFormatted}
|
||||
</CenteredGridCellWrapper>
|
||||
) : (
|
||||
valueFormatted
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Available')}
|
||||
field="available"
|
||||
type="rightAligned"
|
||||
headerTooltip={t(
|
||||
'This is the amount of collateral available in your general account.'
|
||||
)}
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<AccountFields, 'available'>) => {
|
||||
return !data?.available
|
||||
? undefined
|
||||
: toBigNum(data.available, data.asset.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'available'>) =>
|
||||
data &&
|
||||
data.asset &&
|
||||
isNumeric(value) &&
|
||||
addDecimalsFormatNumber(value, data.asset.decimals)
|
||||
isNumeric(data.available) &&
|
||||
addDecimalsFormatNumber(data.available, data.asset.decimals)
|
||||
}
|
||||
maxWidth={300}
|
||||
cellRenderer={({
|
||||
data,
|
||||
node,
|
||||
}: VegaICellRendererParams<AccountFields, 'available'>) => {
|
||||
const valueFormatted =
|
||||
data &&
|
||||
data.asset &&
|
||||
isNumeric(data.available) &&
|
||||
addDecimalsFormatNumber(data.available, data.asset.decimals);
|
||||
return node.rowPinned ? (
|
||||
<CenteredGridCellWrapper className="h-[30px] justify-end">
|
||||
{valueFormatted}
|
||||
</CenteredGridCellWrapper>
|
||||
) : (
|
||||
valueFormatted
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{
|
||||
<AgGridColumn
|
||||
colId="breakdown"
|
||||
headerName=""
|
||||
sortable={false}
|
||||
minWidth={200}
|
||||
type="rightAligned"
|
||||
cellRenderer={({
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields>) => {
|
||||
if (!data) return null;
|
||||
else {
|
||||
if (
|
||||
data.asset.id === pinnedAssetId &&
|
||||
new BigNumber(data.deposited).isLessThanOrEqualTo(0)
|
||||
) {
|
||||
return (
|
||||
<ButtonLink
|
||||
<AgGridColumn
|
||||
colId="breakdown"
|
||||
headerName=""
|
||||
sortable={false}
|
||||
minWidth={200}
|
||||
type="rightAligned"
|
||||
cellRenderer={({
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields>) => {
|
||||
if (!data) return null;
|
||||
else {
|
||||
if (
|
||||
data.asset.id === pinnedAssetId &&
|
||||
new BigNumber(data.deposited).isLessThanOrEqualTo(0)
|
||||
) {
|
||||
return (
|
||||
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="primary"
|
||||
data-testid="deposit"
|
||||
onClick={() => {
|
||||
onClickDeposit && onClickDeposit(data.asset.id);
|
||||
}}
|
||||
>
|
||||
{t('Deposit to trade')}
|
||||
</ButtonLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ButtonLink
|
||||
data-testid="breakdown"
|
||||
onClick={() => {
|
||||
setOpenBreakdown(!openBreakdown);
|
||||
setBreakdown(data.breakdown || null);
|
||||
}}
|
||||
>
|
||||
{t('Breakdown')}
|
||||
</ButtonLink>
|
||||
<span className="mx-1" />
|
||||
{!props.isReadOnly && (
|
||||
<ButtonLink
|
||||
data-testid="deposit"
|
||||
onClick={() => {
|
||||
onClickDeposit && onClickDeposit(data.asset.id);
|
||||
}}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
<span className="mx-1" />
|
||||
{!props.isReadOnly && (
|
||||
<ButtonLink
|
||||
data-testid="withdraw"
|
||||
onClick={() =>
|
||||
onClickWithdraw && onClickWithdraw(data.asset.id)
|
||||
}
|
||||
>
|
||||
{t('Withdraw')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
</>
|
||||
</Button>
|
||||
</CenteredGridCellWrapper>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ButtonLink
|
||||
data-testid="breakdown"
|
||||
onClick={() => {
|
||||
setOpenBreakdown(!openBreakdown);
|
||||
setBreakdown(data.breakdown || null);
|
||||
}}
|
||||
>
|
||||
{t('Breakdown')}
|
||||
</ButtonLink>
|
||||
<span className="mx-1" />
|
||||
{!props.isReadOnly && (
|
||||
<ButtonLink
|
||||
data-testid="deposit"
|
||||
onClick={() => {
|
||||
onClickDeposit && onClickDeposit(data.asset.id);
|
||||
}}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
<span className="mx-1" />
|
||||
{!props.isReadOnly && (
|
||||
<ButtonLink
|
||||
data-testid="withdraw"
|
||||
onClick={() =>
|
||||
onClickWithdraw && onClickWithdraw(data.asset.id)
|
||||
}
|
||||
>
|
||||
{t('Withdraw')}
|
||||
</ButtonLink>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
<Dialog size="medium" open={openBreakdown} onChange={setOpenBreakdown}>
|
||||
<div className="h-[35vh] w-full m-auto flex flex-col">
|
||||
|
||||
@@ -43,10 +43,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
__typename: 'AccountBalance',
|
||||
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '100000000',
|
||||
market: {
|
||||
id: 'market-0',
|
||||
__typename: 'Market',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id-2',
|
||||
@@ -75,7 +71,7 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id-2',
|
||||
id: 'asset-0',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -94,7 +90,7 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '100000000',
|
||||
balance: '10000000000',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
@@ -141,3 +137,25 @@ export const accountEventsSubscription = (
|
||||
};
|
||||
return merge(defaultResult, override);
|
||||
};
|
||||
|
||||
export const amendGeneralAccountBalance = (
|
||||
accounts: AccountsQuery,
|
||||
marketId: string,
|
||||
balance: string
|
||||
) => {
|
||||
if (accounts.party?.accountsConnection?.edges) {
|
||||
const marginAccount = accounts.party.accountsConnection.edges.find(
|
||||
(edge) => edge?.node.market?.id === marketId
|
||||
);
|
||||
if (marginAccount) {
|
||||
const generalAccount = accounts.party.accountsConnection.edges.find(
|
||||
(edge) =>
|
||||
edge?.node.asset.id === marginAccount.node.asset.id &&
|
||||
!edge?.node.market
|
||||
);
|
||||
if (generalAccount) {
|
||||
generalAccount.node.balance = balance;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export const TransferContainer = () => {
|
||||
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: { partyId: pubKey },
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
const create = useVegaTransactionStore((store) => store.create);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { makeDataProvider } from '@vegaprotocol/utils';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { AssetQuery, AssetFieldsFragment } from './__generated__/Asset';
|
||||
import type {
|
||||
AssetQuery,
|
||||
AssetFieldsFragment,
|
||||
AssetQueryVariables,
|
||||
} from './__generated__/Asset';
|
||||
import { AssetDocument } from './__generated__/Asset';
|
||||
|
||||
export type Asset = AssetFieldsFragment;
|
||||
@@ -15,21 +18,21 @@ export const getData = (responseData: AssetQuery | null | undefined) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const assetProvider = makeDataProvider<AssetQuery, Asset, never, never>({
|
||||
export const assetProvider = makeDataProvider<
|
||||
AssetQuery,
|
||||
Asset,
|
||||
never,
|
||||
never,
|
||||
AssetQueryVariables
|
||||
>({
|
||||
query: AssetDocument,
|
||||
getData,
|
||||
});
|
||||
|
||||
export const useAssetDataProvider = (assetId: string) => {
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
assetId,
|
||||
}),
|
||||
[assetId]
|
||||
);
|
||||
return useDataProvider({
|
||||
dataProvider: assetProvider,
|
||||
variables,
|
||||
variables: { assetId: assetId || '' },
|
||||
skip: !assetId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -40,4 +40,5 @@ export const enabledAssetsProvider = makeDerivedDataProvider<
|
||||
export const useAssetsDataProvider = () =>
|
||||
useDataProvider({
|
||||
dataProvider: assetsProvider,
|
||||
variables: undefined,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from './lib/cells/price-cell';
|
||||
export * from './lib/cells/price-change-cell';
|
||||
export * from './lib/cells/price-flash-cell';
|
||||
export * from './lib/cells/vol-cell';
|
||||
export * from './lib/cells/centered-grid-cell';
|
||||
|
||||
export * from './lib/filters/date-range-filter';
|
||||
export * from './lib/filters/set-filter';
|
||||
|
||||
@@ -22,13 +22,15 @@ const agGridDarkVariables = `
|
||||
border-width: 1px 0;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.ag-theme-balham-dark .ag-row.no-hover, .ag-theme-balham-dark .ag-row.no-hover:hover {
|
||||
background: black;
|
||||
}
|
||||
.ag-theme-balham-dark .ag-react-container {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ag-theme-balham-dark .ag-cell, .ag-theme-balham-dark .ag-full-width-row .ag-cell-wrapper.ag-row-group {
|
||||
.ag-theme-balham-dark .ag-cell, .ag-theme-balham-dark .ag-full-width-row .ag-cell-wrapper.ag-row-group {
|
||||
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -22,13 +22,15 @@ const agGridLightVariables = `
|
||||
border-width: 1px 0;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.ag-theme-balham .ag-row.no-hover, .ag-theme-balham .ag-row.no-hover:hover {
|
||||
background: white;
|
||||
}
|
||||
.ag-theme-balham .ag-react-container {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ag-theme-balham .ag-cell, .ag-theme-balham .ag-full-width-row .ag-cell-wrapper.ag-row-group {
|
||||
.ag-theme-balham .ag-cell, .ag-theme-balham .ag-full-width-row .ag-cell-wrapper.ag-row-group {
|
||||
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const CenteredGridCellWrapper = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => (
|
||||
<div
|
||||
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
|
||||
>
|
||||
<div className="self-center">{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { StateCreator } from 'zustand';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
const { create: actualCreate } = jest.requireActual('zustand'); // if using jest
|
||||
|
||||
// a variable to hold reset functions for all stores declared in the app
|
||||
const storeResetFns = new Set<() => void>();
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
export const create =
|
||||
() =>
|
||||
<S>(createState: StateCreator<S>) => {
|
||||
const store = actualCreate(createState);
|
||||
const initialState = store.getState();
|
||||
storeResetFns.add(() => store.setState(initialState, true));
|
||||
return store;
|
||||
};
|
||||
|
||||
// Reset all stores after each test run
|
||||
beforeEach(() => {
|
||||
act(() => storeResetFns.forEach((resetFn) => resetFn()));
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Notification, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { DepositDialog, useDepositDialog } from '@vegaprotocol/deposits';
|
||||
import { useDepositDialog } from '@vegaprotocol/deposits';
|
||||
|
||||
interface Props {
|
||||
margin: string;
|
||||
@@ -16,25 +16,22 @@ interface Props {
|
||||
export const MarginWarning = ({ margin, balance, asset }: Props) => {
|
||||
const openDepositDialog = useDepositDialog((state) => state.open);
|
||||
return (
|
||||
<>
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="dealticket-warning-margin"
|
||||
message={`You may not have enough margin available to open this position. ${formatNumber(
|
||||
margin,
|
||||
asset.decimals
|
||||
)} ${asset.symbol} ${t(
|
||||
'is currently required. You have only'
|
||||
)} ${formatNumber(balance, asset.decimals)} ${asset.symbol} ${t(
|
||||
'available.'
|
||||
)}`}
|
||||
buttonProps={{
|
||||
text: t(`Deposit ${asset.symbol}`),
|
||||
action: () => openDepositDialog(asset.id),
|
||||
dataTestId: 'deal-ticket-deposit-dialog-button',
|
||||
}}
|
||||
/>
|
||||
<DepositDialog />
|
||||
</>
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="dealticket-warning-margin"
|
||||
message={`You may not have enough margin available to open this position. ${addDecimalsFormatNumber(
|
||||
margin,
|
||||
asset.decimals
|
||||
)} ${asset.symbol} ${t(
|
||||
'is currently required. You have only'
|
||||
)} ${addDecimalsFormatNumber(balance, asset.decimals)} ${
|
||||
asset.symbol
|
||||
} ${t('available.')}`}
|
||||
buttonProps={{
|
||||
text: t(`Deposit ${asset.symbol}`),
|
||||
action: () => openDepositDialog(asset.id),
|
||||
dataTestId: 'deal-ticket-deposit-dialog-button',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ interface ZeroBalanceErrorProps {
|
||||
id: string;
|
||||
symbol: string;
|
||||
};
|
||||
onClickCollateral: () => void;
|
||||
onClickCollateral?: () => void;
|
||||
}
|
||||
|
||||
export const ZeroBalanceError = ({
|
||||
@@ -21,8 +21,12 @@ export const ZeroBalanceError = ({
|
||||
testId="dealticket-error-message-zero-balance"
|
||||
message={
|
||||
<>
|
||||
You need {asset.symbol} in your wallet to trade in this market. See
|
||||
all your <Link onClick={onClickCollateral}>collateral</Link>.
|
||||
You need {asset.symbol} in your wallet to trade in this market.
|
||||
{onClickCollateral && (
|
||||
<>
|
||||
See all your <Link onClick={onClickCollateral}>collateral</Link>.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
buttonProps={{
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import type { UseFormRegister } from 'react-hook-form';
|
||||
import type { Control } from 'react-hook-form';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { DealTicketFormFields } from './deal-ticket';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||
|
||||
export interface DealTicketAmountProps {
|
||||
control: Control<OrderFormFields>;
|
||||
orderType: Schema.OrderType;
|
||||
marketData: MarketData;
|
||||
market: Market;
|
||||
register: UseFormRegister<DealTicketFormFields>;
|
||||
sizeError?: string;
|
||||
priceError?: string;
|
||||
update: (obj: Partial<OrderObj>) => void;
|
||||
size: string;
|
||||
price?: string;
|
||||
}
|
||||
|
||||
export const DealTicketAmount = ({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/react-helpers';
|
||||
@@ -29,7 +28,7 @@ export const DealTicketContainer = ({
|
||||
} = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketDataProvider,
|
||||
variables: useMemo(() => ({ marketId }), [marketId]),
|
||||
variables: { marketId },
|
||||
},
|
||||
1000
|
||||
);
|
||||
@@ -47,7 +46,7 @@ export const DealTicketContainer = ({
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
onClickCollateral={onClickCollateral || (() => null)}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
@@ -12,22 +11,51 @@ interface DealTicketFeeDetailsProps {
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
margin: string;
|
||||
totalMargin: string;
|
||||
balance: string;
|
||||
}
|
||||
|
||||
export interface DealTicketFeeDetails {
|
||||
export interface DealTicketFeeDetailProps {
|
||||
label: string;
|
||||
value?: string | number | null;
|
||||
labelDescription?: string | ReactNode;
|
||||
symbol?: string;
|
||||
}
|
||||
|
||||
export const DealTicketFeeDetail = ({
|
||||
label,
|
||||
value,
|
||||
labelDescription,
|
||||
symbol,
|
||||
}: DealTicketFeeDetailProps) => (
|
||||
<div className="text-xs mt-2 flex justify-between items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div>{label}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-neutral-500 dark:text-neutral-300">{`${value ?? '-'} ${
|
||||
symbol || ''
|
||||
}`}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DealTicketFeeDetails = ({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
margin,
|
||||
totalMargin,
|
||||
balance,
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const feeDetails = useFeeDealTicketDetails(order, market, marketData);
|
||||
const details = useMemo(() => getFeeDetailsValues(feeDetails), [feeDetails]);
|
||||
const details = getFeeDetailsValues({
|
||||
...feeDetails,
|
||||
margin,
|
||||
totalMargin,
|
||||
balance,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{details.map(({ label, value, labelDescription, symbol }) => (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
export type DealTicketLimitAmountProps = Omit<
|
||||
Omit<DealTicketAmountProps, 'marketData'>,
|
||||
@@ -9,10 +10,13 @@ export type DealTicketLimitAmountProps = Omit<
|
||||
>;
|
||||
|
||||
export const DealTicketLimitAmount = ({
|
||||
register,
|
||||
control,
|
||||
market,
|
||||
sizeError,
|
||||
priceError,
|
||||
update,
|
||||
price,
|
||||
size,
|
||||
}: DealTicketLimitAmountProps) => {
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -47,22 +51,30 @@ export const DealTicketLimitAmount = ({
|
||||
labelFor="input-order-size-limit"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Input
|
||||
id="input-order-size-limit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...register('size', {
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
})}
|
||||
}}
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-size-limit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={size}
|
||||
onChange={(e) => update({ size: e.target.value })}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
@@ -77,14 +89,10 @@ export const DealTicketLimitAmount = ({
|
||||
labelAlign="right"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Input
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...register('price', {
|
||||
<Controller
|
||||
name="price"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
@@ -92,7 +100,19 @@ export const DealTicketLimitAmount = ({
|
||||
},
|
||||
// @ts-ignore this fulfills the interface but still errors
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
})}
|
||||
}}
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(e) => update({ price: e.target.value })}
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { isMarketInAuction } from '../../utils';
|
||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { getMarketPrice } from '../../utils/get-price';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
export type DealTicketMarketAmountProps = Omit<
|
||||
DealTicketAmountProps,
|
||||
@@ -15,10 +16,12 @@ export type DealTicketMarketAmountProps = Omit<
|
||||
>;
|
||||
|
||||
export const DealTicketMarketAmount = ({
|
||||
register,
|
||||
control,
|
||||
market,
|
||||
marketData,
|
||||
sizeError,
|
||||
update,
|
||||
size,
|
||||
}: DealTicketMarketAmountProps) => {
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -47,22 +50,30 @@ export const DealTicketMarketAmount = ({
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="input-order-size-market"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
{...register('size', {
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
})}
|
||||
}}
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-size-market"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={size}
|
||||
onChange={(e) => update({ size: e.target.value })}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>@</div>
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { fireEvent, render, screen, act } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { generateMarket, generateMarketData } from '../../test-helpers';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { ChainIdQuery } from '@vegaprotocol/react-helpers';
|
||||
import { ChainIdDocument } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { useOrderStore } from '@vegaprotocol/orders';
|
||||
|
||||
jest.mock('zustand');
|
||||
jest.mock('./deal-ticket-fee-details', () => ({
|
||||
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||
}));
|
||||
|
||||
const pubKey = 'pubKey';
|
||||
const market = generateMarket();
|
||||
const marketData = generateMarketData();
|
||||
const submit = jest.fn();
|
||||
|
||||
const mockChainId = 'chain-id';
|
||||
|
||||
function generateJsx(order?: OrderSubmissionBody['orderSubmission']) {
|
||||
const chainIdMock: MockedResponse<ChainIdQuery> = {
|
||||
request: {
|
||||
query: ChainIdDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
statistics: {
|
||||
chainId: mockChainId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
function generateJsx() {
|
||||
return (
|
||||
<MockedProvider mocks={[chainIdMock]}>
|
||||
<VegaWalletContext.Provider value={{ pubKey: mockChainId } as any}>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={{ pubKey, isReadOnly: false } as any}>
|
||||
<DealTicket market={market} marketData={marketData} submit={submit} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
@@ -41,10 +31,11 @@ function generateJsx(order?: OrderSubmissionBody['orderSubmission']) {
|
||||
|
||||
describe('DealTicket', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -61,9 +52,7 @@ describe('DealTicket', () => {
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
String(1 / Math.pow(10, market.positionDecimalPlaces))
|
||||
);
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
@@ -76,7 +65,49 @@ describe('DealTicket', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('handles TIF select box dependent on order type', () => {
|
||||
it('should use local storage state for initial values', () => {
|
||||
const expectedOrder = {
|
||||
marketId: market.id,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '0.1',
|
||||
price: '300.22',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
persist: true,
|
||||
};
|
||||
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
expectedOrder.timeInForce
|
||||
);
|
||||
expect(screen.getByTestId('order-price')).toHaveDisplayValue(
|
||||
expectedOrder.price
|
||||
);
|
||||
});
|
||||
|
||||
it('handles TIF select box dependent on order type', async () => {
|
||||
render(generateJsx());
|
||||
|
||||
// Only FOK and IOC should be present by default (type market order)
|
||||
@@ -86,50 +117,72 @@ describe('DealTicket', () => {
|
||||
)
|
||||
).toEqual(['Fill or Kill (FOK)', 'Immediate or Cancel (IOC)']);
|
||||
|
||||
// IOC should be default
|
||||
expect(screen.getByTestId('order-tif')).toHaveDisplayValue(
|
||||
'Immediate or Cancel (IOC)'
|
||||
);
|
||||
|
||||
// Select FOK - FOK should be selected
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveDisplayValue(
|
||||
'Fill or Kill (FOK)'
|
||||
);
|
||||
|
||||
// Switch to type limit order -> all TIF options should be shown
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||
Object.keys(Schema.OrderTimeInForce).length
|
||||
);
|
||||
|
||||
// Select GTC -> GTC should be selected
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC },
|
||||
});
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||
);
|
||||
|
||||
// Switch to type market order -> IOC should be selected (default)
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Select IOC -> IOC should be selected
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC },
|
||||
});
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Switch to type limit order -> GTC should be selected
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
// expect GTC as LIMIT default
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||
);
|
||||
|
||||
// Select GTT -> GTT should be selected
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT },
|
||||
});
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
|
||||
// Switch to type market order -> IOC should be selected
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
// Switch back to type market order -> FOK should be preserved from previous selection
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||
);
|
||||
|
||||
// Select IOC -> IOC should be selected
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Switch back type limit order -> GTT should be preserved
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
|
||||
// Select GFN -> GFN should be selected
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN
|
||||
);
|
||||
|
||||
// Switch to type market order -> IOC should be preserved
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
@@ -143,23 +196,20 @@ describe('DealTicket', () => {
|
||||
screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('order-size'), {
|
||||
target: { value: '200' },
|
||||
});
|
||||
});
|
||||
await userEvent.type(screen.getByTestId('order-size'), '200');
|
||||
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
|
||||
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC },
|
||||
});
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Switch to limit order
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
|
||||
// Check all TIF options shown
|
||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { DealTicketAmount } from './deal-ticket-amount';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
@@ -9,20 +9,21 @@ import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
normalizeOrderSubmission,
|
||||
useVegaWallet,
|
||||
useVegaWalletDialogStore,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
ExternalLink,
|
||||
InputError,
|
||||
Intent,
|
||||
Notification,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
|
||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||
|
||||
import {
|
||||
getDefaultOrder,
|
||||
validateExpiration,
|
||||
validateMarketState,
|
||||
validateMarketTradingMode,
|
||||
validateTimeInForce,
|
||||
@@ -30,29 +31,24 @@ import {
|
||||
} from '../../utils';
|
||||
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
||||
import { SummaryValidationType } from '../../constants';
|
||||
import { useHasNoBalance } from '../../hooks/use-has-no-balance';
|
||||
import { useInitialMargin } from '../../hooks/use-initial-margin';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||
import {
|
||||
usePersistedOrderStore,
|
||||
usePersistedOrderStoreSubscription,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
useMarketAccountBalance,
|
||||
useAccountBalance,
|
||||
} from '@vegaprotocol/accounts';
|
||||
|
||||
export type TransactionStatus = 'default' | 'pending';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { useOrderForm } from '../../hooks/use-order-form';
|
||||
|
||||
export interface DealTicketProps {
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
submit: (order: OrderSubmissionBody['orderSubmission']) => void;
|
||||
submit: (order: OrderSubmission) => void;
|
||||
onClickCollateral?: () => void;
|
||||
}
|
||||
|
||||
export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
|
||||
// This is not a field used in the form but allows us to set a
|
||||
// summary error message
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
marketData,
|
||||
@@ -60,58 +56,58 @@ export const DealTicket = ({
|
||||
onClickCollateral,
|
||||
}: DealTicketProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore(
|
||||
(store) => ({
|
||||
getPersistedOrder: store.getOrder,
|
||||
setPersistedOrder: store.setOrder,
|
||||
})
|
||||
);
|
||||
// store last used tif for market so that when changing OrderType the previous TIF
|
||||
// selection for that type is used when switching back
|
||||
|
||||
const [lastTIF, setLastTIF] = useState({
|
||||
[OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
[OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
errors,
|
||||
order,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
} = useForm<DealTicketFormFields>({
|
||||
defaultValues: getPersistedOrder(market.id) || getDefaultOrder(market),
|
||||
});
|
||||
update,
|
||||
handleSubmit,
|
||||
} = useOrderForm(market.id);
|
||||
|
||||
const order = watch();
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
|
||||
watch((orderData) => {
|
||||
const persistable = !(
|
||||
orderData.type === OrderType.TYPE_LIMIT && orderData.price === ''
|
||||
const { accountBalance: marginAccountBalance } = useMarketAccountBalance(
|
||||
market.id
|
||||
);
|
||||
|
||||
const { accountBalance: generalAccountBalance } = useAccountBalance(asset.id);
|
||||
|
||||
const balance = (
|
||||
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
|
||||
).toString();
|
||||
|
||||
const { marketState, marketTradingMode } = marketData;
|
||||
|
||||
const normalizedOrder =
|
||||
order &&
|
||||
normalizeOrderSubmission(
|
||||
order,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
if (persistable) {
|
||||
setPersistedOrder(orderData as DealTicketFormFields);
|
||||
}
|
||||
});
|
||||
|
||||
usePersistedOrderStoreSubscription(market.id, (storedOrder) => {
|
||||
if (order.price !== storedOrder.price) {
|
||||
clearErrors('price');
|
||||
setValue('price', storedOrder.price);
|
||||
}
|
||||
});
|
||||
const { margin, totalMargin } = useInitialMargin(market.id, normalizedOrder);
|
||||
|
||||
const marketStateError = validateMarketState(marketData.marketState);
|
||||
const hasNoBalance = useHasNoBalance(
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id
|
||||
);
|
||||
const marketTradingModeError = validateMarketTradingMode(
|
||||
marketData.marketTradingMode
|
||||
);
|
||||
|
||||
const checkForErrors = useCallback(() => {
|
||||
useEffect(() => {
|
||||
if (!pubKey) {
|
||||
setError('summary', { message: t('No public key selected') });
|
||||
setError('summary', {
|
||||
message: t('No public key selected'),
|
||||
type: SummaryValidationType.NoPubKey,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const marketStateError = validateMarketState(marketState);
|
||||
if (marketStateError !== true) {
|
||||
setError('summary', {
|
||||
message: marketStateError,
|
||||
@@ -120,6 +116,7 @@ export const DealTicket = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNoBalance = generalAccountBalance === '0';
|
||||
if (hasNoBalance) {
|
||||
setError('summary', {
|
||||
message: SummaryValidationType.NoCollateral,
|
||||
@@ -128,6 +125,7 @@ export const DealTicket = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
|
||||
if (marketTradingModeError !== true) {
|
||||
setError('summary', {
|
||||
message: marketTradingModeError,
|
||||
@@ -135,39 +133,19 @@ export const DealTicket = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
clearErrors('summary');
|
||||
}, [
|
||||
hasNoBalance,
|
||||
marketStateError,
|
||||
marketTradingModeError,
|
||||
marketState,
|
||||
marketTradingMode,
|
||||
generalAccountBalance,
|
||||
pubKey,
|
||||
setError,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(!hasNoBalance &&
|
||||
errors.summary?.type === SummaryValidationType.NoCollateral) ||
|
||||
(marketStateError === true &&
|
||||
errors.summary?.type === SummaryValidationType.MarketState) ||
|
||||
(marketTradingModeError === true &&
|
||||
errors.summary?.type === SummaryValidationType.TradingMode)
|
||||
) {
|
||||
clearErrors('summary');
|
||||
}
|
||||
checkForErrors();
|
||||
}, [
|
||||
hasNoBalance,
|
||||
marketStateError,
|
||||
marketTradingModeError,
|
||||
clearErrors,
|
||||
errors.summary?.message,
|
||||
errors.summary?.type,
|
||||
checkForErrors,
|
||||
errors.summary,
|
||||
]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(order: OrderSubmissionBody['orderSubmission']) => {
|
||||
checkForErrors();
|
||||
(order: OrderSubmission) => {
|
||||
submit(
|
||||
normalizeOrderSubmission(
|
||||
order,
|
||||
@@ -176,12 +154,15 @@ export const DealTicket = ({
|
||||
)
|
||||
);
|
||||
},
|
||||
[checkForErrors, submit, market.decimalPlaces, market.positionDecimalPlaces]
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces]
|
||||
);
|
||||
|
||||
// if an order doesn't exist one will be created by the store immediately
|
||||
if (!order || !normalizedOrder) return null;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={isReadOnly ? () => null : handleSubmit(onSubmit)}
|
||||
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
|
||||
className="p-4"
|
||||
noValidate
|
||||
>
|
||||
@@ -194,10 +175,17 @@ export const DealTicket = ({
|
||||
marketData.trigger
|
||||
),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<TypeSelector
|
||||
value={field.value}
|
||||
onSelect={field.onChange}
|
||||
value={order.type}
|
||||
onSelect={(type) => {
|
||||
if (type === OrderType.TYPE_NETWORK) return;
|
||||
update({
|
||||
type,
|
||||
// when changing type also update the tif to what was last used of new type
|
||||
timeInForce: lastTIF[type] || order.timeInForce,
|
||||
});
|
||||
}}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.type?.message}
|
||||
@@ -207,17 +195,25 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="side"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SideSelector value={field.value} onSelect={field.onChange} />
|
||||
render={() => (
|
||||
<SideSelector
|
||||
value={order.side}
|
||||
onSelect={(side) => {
|
||||
update({ side });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DealTicketAmount
|
||||
control={control}
|
||||
orderType={order.type}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
register={register}
|
||||
sizeError={errors.size?.message}
|
||||
priceError={errors.price?.message}
|
||||
update={update}
|
||||
size={order.size}
|
||||
price={order.price}
|
||||
/>
|
||||
<Controller
|
||||
name="timeInForce"
|
||||
@@ -228,11 +224,16 @@ export const DealTicket = ({
|
||||
marketData.trigger
|
||||
),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<TimeInForceSelector
|
||||
value={field.value}
|
||||
value={order.timeInForce}
|
||||
orderType={order.type}
|
||||
onSelect={field.onChange}
|
||||
onSelect={(timeInForce) => {
|
||||
update({ timeInForce });
|
||||
// Set tif value for the given order type, so that when switching
|
||||
// types we know the last used TIF for the given order type
|
||||
setLastTIF((curr) => ({ ...curr, [order.type]: timeInForce }));
|
||||
}}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.timeInForce?.message}
|
||||
@@ -244,33 +245,43 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="expiresAt"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
rules={{
|
||||
validate: validateExpiration,
|
||||
}}
|
||||
render={() => (
|
||||
<ExpirySelector
|
||||
value={field.value}
|
||||
onSelect={field.onChange}
|
||||
value={order.expiresAt}
|
||||
onSelect={(expiresAt) =>
|
||||
update({
|
||||
expiresAt: expiresAt || undefined,
|
||||
})
|
||||
}
|
||||
errorMessage={errors.expiresAt?.message}
|
||||
register={register}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<SummaryMessage
|
||||
errorMessage={errors.summary?.message}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
order={order}
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={balance}
|
||||
margin={totalMargin}
|
||||
isReadOnly={isReadOnly}
|
||||
pubKey={pubKey}
|
||||
onClickCollateral={onClickCollateral || (() => null)}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
<DealTicketButton
|
||||
disabled={Object.keys(errors).length >= 1 || isReadOnly}
|
||||
variant={order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'}
|
||||
/>
|
||||
<DealTicketFeeDetails
|
||||
order={order}
|
||||
order={normalizedOrder}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
margin={margin}
|
||||
totalMargin={totalMargin}
|
||||
balance={marginAccountBalance}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
@@ -282,32 +293,28 @@ export const DealTicket = ({
|
||||
*/
|
||||
interface SummaryMessageProps {
|
||||
errorMessage?: string;
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
asset: { id: string; symbol: string; name: string; decimals: number };
|
||||
marketTradingMode: MarketData['marketTradingMode'];
|
||||
balance: string;
|
||||
margin: string;
|
||||
isReadOnly: boolean;
|
||||
pubKey: string | null;
|
||||
onClickCollateral: () => void;
|
||||
onClickCollateral?: () => void;
|
||||
}
|
||||
const SummaryMessage = memo(
|
||||
({
|
||||
errorMessage,
|
||||
market,
|
||||
marketData,
|
||||
order,
|
||||
asset,
|
||||
marketTradingMode,
|
||||
balance,
|
||||
margin,
|
||||
isReadOnly,
|
||||
pubKey,
|
||||
onClickCollateral,
|
||||
}: SummaryMessageProps) => {
|
||||
// Specific error UI for if balance is so we can
|
||||
// render a deposit dialog
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
const assetSymbol = asset.symbol;
|
||||
const { balanceError, balance, margin } = useOrderMarginValidation({
|
||||
market,
|
||||
marketData,
|
||||
order,
|
||||
});
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
@@ -351,7 +358,7 @@ const SummaryMessage = memo(
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<ZeroBalanceError
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
asset={asset}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
</div>
|
||||
@@ -372,21 +379,16 @@ const SummaryMessage = memo(
|
||||
|
||||
// If there is no blocking error but user doesn't have enough
|
||||
// balance render the margin warning, but still allow submission
|
||||
if (balanceError) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<MarginWarning balance={balance} margin={margin} asset={asset} />
|
||||
</div>
|
||||
);
|
||||
if (BigInt(balance) < BigInt(margin)) {
|
||||
return <MarginWarning balance={balance} margin={margin} asset={asset} />;
|
||||
}
|
||||
|
||||
// Show auction mode warning
|
||||
if (
|
||||
[
|
||||
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
].includes(marketData.marketTradingMode)
|
||||
].includes(marketTradingMode)
|
||||
) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatForInput } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { UseFormRegister } from 'react-hook-form';
|
||||
import { validateExpiration } from '../../utils/validate-expiration';
|
||||
import type { DealTicketFormFields } from '.';
|
||||
|
||||
interface ExpirySelectorProps {
|
||||
value?: string;
|
||||
onSelect: (expiration: string | null) => void;
|
||||
errorMessage?: string;
|
||||
register?: UseFormRegister<DealTicketFormFields>;
|
||||
}
|
||||
|
||||
export const ExpirySelector = ({
|
||||
value,
|
||||
onSelect,
|
||||
errorMessage,
|
||||
register,
|
||||
}: ExpirySelectorProps) => {
|
||||
const date = value ? new Date(value) : new Date();
|
||||
const dateFormatted = formatForInput(date);
|
||||
const minDate = formatForInput(date);
|
||||
return (
|
||||
<FormGroup label={t('Expiry time/date')} labelFor="expiration">
|
||||
<FormGroup
|
||||
label={t('Expiry time/date')}
|
||||
labelFor="expiration"
|
||||
compact={true}
|
||||
>
|
||||
<Input
|
||||
data-testid="date-picker-field"
|
||||
id="expiration"
|
||||
@@ -30,9 +29,6 @@ export const ExpirySelector = ({
|
||||
value={dateFormatted}
|
||||
onChange={(e) => onSelect(e.target.value)}
|
||||
min={minDate}
|
||||
{...register?.('expiresAt', {
|
||||
validate: validateExpiration,
|
||||
})}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<InputError testId="dealticket-error-message-expiry">
|
||||
|
||||
@@ -55,6 +55,7 @@ export const MarketSelector = ({ market, setMarket, ItemRenderer }: Props) => {
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketsProvider,
|
||||
variables: undefined,
|
||||
skipUpdates: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup label={t('Direction')} labelFor="order-side-toggle">
|
||||
<FormGroup
|
||||
label={t('Direction')}
|
||||
labelFor="order-side-toggle"
|
||||
compact={true}
|
||||
>
|
||||
<Toggle
|
||||
id="order-side-toggle"
|
||||
name="order-side"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
FormGroup,
|
||||
InputError,
|
||||
@@ -22,15 +21,6 @@ interface TimeInForceSelectorProps {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
type OrderType = Schema.OrderType.TYPE_MARKET | Schema.OrderType.TYPE_LIMIT;
|
||||
type PreviousTimeInForce = {
|
||||
[key in OrderType]: Schema.OrderTimeInForce;
|
||||
};
|
||||
const DEFAULT_TIME_IN_FORCE: PreviousTimeInForce = {
|
||||
[Schema.OrderType.TYPE_MARKET]: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
[Schema.OrderType.TYPE_LIMIT]: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
};
|
||||
|
||||
export const TimeInForceSelector = ({
|
||||
value,
|
||||
orderType,
|
||||
@@ -47,28 +37,6 @@ export const TimeInForceSelector = ({
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
const [previousOrderType, setPreviousOrderType] = useState(
|
||||
Schema.OrderType.TYPE_MARKET
|
||||
);
|
||||
const [previousTimeInForce, setPreviousTimeInForce] =
|
||||
useState<PreviousTimeInForce>({
|
||||
...DEFAULT_TIME_IN_FORCE,
|
||||
[orderType]: value,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (previousOrderType !== orderType) {
|
||||
setPreviousOrderType(orderType);
|
||||
const prev = previousTimeInForce[orderType as OrderType];
|
||||
onSelect(prev);
|
||||
}
|
||||
}, [
|
||||
onSelect,
|
||||
orderType,
|
||||
previousTimeInForce,
|
||||
previousOrderType,
|
||||
setPreviousOrderType,
|
||||
]);
|
||||
|
||||
const renderError = (errorType: string) => {
|
||||
if (errorType === MarketModeValidationType.Auction) {
|
||||
@@ -119,15 +87,25 @@ export const TimeInForceSelector = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup label={t('Time in force')} labelFor="select-time-in-force">
|
||||
<FormGroup
|
||||
label={t('Time in force')}
|
||||
labelFor="select-time-in-force"
|
||||
compact={true}
|
||||
>
|
||||
<Select
|
||||
id="select-time-in-force"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setPreviousTimeInForce({
|
||||
...previousTimeInForce,
|
||||
[orderType]: e.target.value,
|
||||
});
|
||||
// setPreviousTimeInForce({
|
||||
// ...previousTimeInForce,
|
||||
// [orderType]: e.target.value,
|
||||
// });
|
||||
|
||||
// if (previousOrderType !== orderType) {
|
||||
// setPreviousOrderType(orderType);
|
||||
// const prev = previousTimeInForce[orderType as OrderType];
|
||||
// onSelect(prev);
|
||||
// }
|
||||
onSelect(e.target.value as Schema.OrderTimeInForce);
|
||||
}}
|
||||
className="w-full"
|
||||
|
||||
@@ -74,7 +74,7 @@ export const TypeSelector = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup label={t('Order type')} labelFor="order-type">
|
||||
<FormGroup label={t('Order type')} labelFor="order-type" compact={true}>
|
||||
<Toggle
|
||||
id="order-type"
|
||||
name="order-type"
|
||||
|
||||
@@ -7,6 +7,15 @@ export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
For example, for a notional size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.`,
|
||||
[settlementAsset]
|
||||
);
|
||||
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t(
|
||||
'Estimated total margin that will cover open position, active orders and this order.'
|
||||
);
|
||||
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance');
|
||||
export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
t(
|
||||
"The additional margin required for your new position (taking into account volume and open orders), compared to your current margin. Measured in the market's settlement asset ($s).",
|
||||
[settlementAsset]
|
||||
);
|
||||
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t(
|
||||
'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.'
|
||||
);
|
||||
@@ -41,6 +50,7 @@ export enum MarketModeValidationType {
|
||||
}
|
||||
|
||||
export enum SummaryValidationType {
|
||||
NoPubKey = 'NoPubKey',
|
||||
NoCollateral = 'NoCollateral',
|
||||
TradingMode = 'MarketTradingMode',
|
||||
MarketState = 'MarketState',
|
||||
|
||||
@@ -4,5 +4,3 @@ export * from './use-fee-deal-ticket-details';
|
||||
export * from './use-market-positions';
|
||||
export * from './use-maximum-position-size';
|
||||
export * from './use-order-closeout';
|
||||
export * from './use-order-margin';
|
||||
export * from './use-order-margin-validation';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
import { marketDepthProvider } from '@vegaprotocol/market-depth';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
@@ -13,11 +12,10 @@ interface Props {
|
||||
}
|
||||
|
||||
export const useCalculateSlippage = ({ market, order }: Props) => {
|
||||
const variables = useMemo(() => ({ marketId: market.id }), [market.id]);
|
||||
const { data } = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketDepthProvider,
|
||||
variables,
|
||||
variables: { marketId: market.id },
|
||||
},
|
||||
1000
|
||||
);
|
||||
|
||||
@@ -3,24 +3,26 @@ import {
|
||||
addDecimal,
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
EST_CLOSEOUT_TOOLTIP_TEXT,
|
||||
EST_MARGIN_TOOLTIP_TEXT,
|
||||
// EST_MARGIN_TOOLTIP_TEXT,
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
NOTIONAL_SIZE_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
} from '../constants';
|
||||
import { useCalculateSlippage } from './use-calculate-slippage';
|
||||
import { useOrderCloseOut } from './use-order-closeout';
|
||||
import { useOrderMargin } from './use-order-margin';
|
||||
import type { OrderMargin } from './use-order-margin';
|
||||
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
|
||||
import { getDerivedPrice } from '../utils/get-price';
|
||||
import { useEstimateOrderQuery } from './__generated__/EstimateOrder';
|
||||
import type { EstimateOrderQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
export const useFeeDealTicketDetails = (
|
||||
order: OrderSubmissionBody['orderSubmission'],
|
||||
@@ -28,33 +30,23 @@ export const useFeeDealTicketDetails = (
|
||||
marketData: MarketData
|
||||
) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const slippage = useCalculateSlippage({ market, order });
|
||||
const { accountBalance } = useMarketAccountBalance(market.id);
|
||||
|
||||
const derivedPrice = useMemo(() => {
|
||||
return getDerivedPrice(order, market, marketData);
|
||||
}, [order, market, marketData]);
|
||||
const price = useMemo(() => {
|
||||
return getDerivedPrice(order, marketData);
|
||||
}, [order, marketData]);
|
||||
|
||||
// Note this isn't currently used anywhere
|
||||
const slippageAdjustedPrice = useMemo(() => {
|
||||
if (derivedPrice) {
|
||||
if (slippage && parseFloat(slippage) !== 0) {
|
||||
const isLong = order.side === Schema.Side.SIDE_BUY;
|
||||
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
|
||||
parseFloat(slippage) / 100
|
||||
);
|
||||
return new BigNumber(derivedPrice).multipliedBy(multiplier).toNumber();
|
||||
}
|
||||
return derivedPrice;
|
||||
}
|
||||
return null;
|
||||
}, [derivedPrice, order.side, slippage]);
|
||||
|
||||
const estMargin = useOrderMargin({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
partyId: pubKey || '',
|
||||
derivedPrice,
|
||||
const { data: estMargin } = useEstimateOrderQuery({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
partyId: pubKey || '',
|
||||
price,
|
||||
size: order.size,
|
||||
side: order.side,
|
||||
timeInForce: order.timeInForce,
|
||||
type: order.type,
|
||||
},
|
||||
skip: !pubKey || !market || !order.size || !price,
|
||||
});
|
||||
|
||||
const estCloseOut = useOrderCloseOut({
|
||||
@@ -64,13 +56,13 @@ export const useFeeDealTicketDetails = (
|
||||
});
|
||||
|
||||
const notionalSize = useMemo(() => {
|
||||
if (derivedPrice && order.size) {
|
||||
return new BigNumber(order.size)
|
||||
.multipliedBy(addDecimal(derivedPrice, market.decimalPlaces))
|
||||
if (price && order.size) {
|
||||
return toBigNum(order.size, market.positionDecimalPlaces)
|
||||
.multipliedBy(addDecimal(price, market.decimalPlaces))
|
||||
.toString();
|
||||
}
|
||||
return null;
|
||||
}, [derivedPrice, order.size, market.decimalPlaces]);
|
||||
}, [price, order.size, market.decimalPlaces, market.positionDecimalPlaces]);
|
||||
|
||||
const assetSymbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
@@ -80,37 +72,40 @@ export const useFeeDealTicketDetails = (
|
||||
market,
|
||||
assetSymbol,
|
||||
notionalSize,
|
||||
estMargin,
|
||||
accountBalance,
|
||||
estimateOrder: estMargin?.estimateOrder,
|
||||
estCloseOut,
|
||||
slippage,
|
||||
slippageAdjustedPrice,
|
||||
};
|
||||
}, [
|
||||
market,
|
||||
assetSymbol,
|
||||
notionalSize,
|
||||
accountBalance,
|
||||
estMargin,
|
||||
estCloseOut,
|
||||
slippage,
|
||||
slippageAdjustedPrice,
|
||||
]);
|
||||
};
|
||||
|
||||
export interface FeeDetails {
|
||||
balance: string;
|
||||
market: Market;
|
||||
assetSymbol: string;
|
||||
notionalSize: string | null;
|
||||
estMargin: OrderMargin | null;
|
||||
estCloseOut: string | null;
|
||||
slippage: string | null;
|
||||
estimateOrder: EstimateOrderQuery['estimateOrder'] | undefined;
|
||||
margin: string;
|
||||
totalMargin: string;
|
||||
}
|
||||
|
||||
export const getFeeDetailsValues = ({
|
||||
balance,
|
||||
assetSymbol,
|
||||
notionalSize,
|
||||
estMargin,
|
||||
estCloseOut,
|
||||
estimateOrder,
|
||||
margin,
|
||||
market,
|
||||
notionalSize,
|
||||
totalMargin,
|
||||
}: FeeDetails) => {
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
@@ -129,7 +124,12 @@ export const getFeeDetailsValues = ({
|
||||
? addDecimalsFormatNumber(value, assetDecimals)
|
||||
: '-';
|
||||
};
|
||||
return [
|
||||
const details: {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
symbol: string;
|
||||
labelDescription: React.ReactNode;
|
||||
}[] = [
|
||||
{
|
||||
label: t('Notional'),
|
||||
value: formatValueWithMarketDp(notionalSize),
|
||||
@@ -139,8 +139,8 @@ export const getFeeDetailsValues = ({
|
||||
{
|
||||
label: t('Fees'),
|
||||
value:
|
||||
estMargin?.totalFees &&
|
||||
`~${formatValueWithAssetDp(estMargin?.totalFees)}`,
|
||||
estimateOrder?.totalFeeAmount &&
|
||||
`~${formatValueWithAssetDp(estimateOrder?.totalFeeAmount)}`,
|
||||
labelDescription: (
|
||||
<>
|
||||
<span>
|
||||
@@ -149,7 +149,7 @@ export const getFeeDetailsValues = ({
|
||||
)}
|
||||
</span>
|
||||
<FeesBreakdown
|
||||
fees={estMargin?.fees}
|
||||
fees={estimateOrder?.fee}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
@@ -158,18 +158,46 @@ export const getFeeDetailsValues = ({
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
},
|
||||
/*
|
||||
{
|
||||
label: t('Margin'),
|
||||
value:
|
||||
estMargin?.margin && `~${formatValueWithAssetDp(estMargin?.margin)}`,
|
||||
label: t('Initial margin'),
|
||||
value: margin && `~${formatValueWithAssetDp(margin)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: EST_MARGIN_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
*/
|
||||
{
|
||||
label: t('Liquidation'),
|
||||
value: estCloseOut && `~${formatValueWithMarketDp(estCloseOut)}`,
|
||||
symbol: market.tradableInstrument.instrument.product.quoteName,
|
||||
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT(quoteName),
|
||||
label: t('Margin required'),
|
||||
value: `~${formatValueWithAssetDp(
|
||||
balance
|
||||
? (BigInt(totalMargin) - BigInt(balance)).toString()
|
||||
: totalMargin
|
||||
)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
];
|
||||
if (balance) {
|
||||
details.push({
|
||||
label: t('Projected margin'),
|
||||
value: `~${formatValueWithAssetDp(totalMargin)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
});
|
||||
}
|
||||
details.push({
|
||||
label: t('Current margin allocation'),
|
||||
value: balance
|
||||
? `~${formatValueWithAssetDp(balance)}`
|
||||
: `${formatValueWithAssetDp(balance)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
});
|
||||
details.push({
|
||||
label: t('Liquidation'),
|
||||
value: estCloseOut && `~${formatValueWithMarketDp(estCloseOut)}`,
|
||||
symbol: market.tradableInstrument.instrument.product.quoteName,
|
||||
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT(quoteName),
|
||||
});
|
||||
return details;
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
|
||||
export const useHasNoBalance = (assetId: string) => {
|
||||
const { accountBalance, accountDecimals } = useAccountBalance(assetId);
|
||||
const balance =
|
||||
accountBalance && accountDecimals !== null
|
||||
? toBigNum(accountBalance, accountDecimals)
|
||||
: toBigNum('0', 0);
|
||||
return balance.isZero();
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { marketDataProvider } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
calculateMargins,
|
||||
// getDerivedPrice,
|
||||
volumeAndMarginProvider,
|
||||
} from '@vegaprotocol/positions';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { marketInfoProvider } from '@vegaprotocol/market-info';
|
||||
|
||||
export const useInitialMargin = (
|
||||
marketId: OrderSubmissionBody['orderSubmission']['marketId'],
|
||||
order?: OrderSubmissionBody['orderSubmission']
|
||||
) => {
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const commonVariables = { marketId, partyId: partyId || '' };
|
||||
const { data: marketData } = useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
variables: { marketId },
|
||||
});
|
||||
const { data: activeVolumeAndMargin } = useDataProvider({
|
||||
dataProvider: volumeAndMarginProvider,
|
||||
variables: commonVariables,
|
||||
skip: !partyId,
|
||||
});
|
||||
const { data: marketInfo } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
variables: commonVariables,
|
||||
});
|
||||
let totalMargin = '0';
|
||||
let margin = '0';
|
||||
if (marketInfo?.riskFactors && marketData && order) {
|
||||
const {
|
||||
positionDecimalPlaces,
|
||||
decimalPlaces,
|
||||
tradableInstrument,
|
||||
riskFactors,
|
||||
} = marketInfo;
|
||||
const { marginCalculator, instrument } = tradableInstrument;
|
||||
const { decimals } = instrument.product.settlementAsset;
|
||||
margin = totalMargin = calculateMargins({
|
||||
side: order.side,
|
||||
size: order.size,
|
||||
price: marketData.markPrice, // getDerivedPrice(order, marketData), same in positions-data-providers
|
||||
positionDecimalPlaces,
|
||||
decimalPlaces,
|
||||
decimals,
|
||||
scalingFactors: marginCalculator?.scalingFactors,
|
||||
riskFactors,
|
||||
}).initialMargin;
|
||||
}
|
||||
|
||||
if (activeVolumeAndMargin) {
|
||||
let sellMargin = BigInt(activeVolumeAndMargin.sellInitialMargin);
|
||||
let buyMargin = BigInt(activeVolumeAndMargin.buyInitialMargin);
|
||||
if (order?.side === Side.SIDE_SELL) {
|
||||
sellMargin += BigInt(totalMargin);
|
||||
} else {
|
||||
buyMargin += BigInt(totalMargin);
|
||||
}
|
||||
totalMargin =
|
||||
sellMargin > buyMargin ? sellMargin.toString() : buyMargin.toString();
|
||||
}
|
||||
|
||||
return useMemo(() => ({ totalMargin, margin }), [totalMargin, margin]);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import omit from 'lodash/omit';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { getDefaultOrder, useOrderStore } from '@vegaprotocol/orders';
|
||||
import { useOrderForm } from './use-order-form';
|
||||
|
||||
jest.mock('zustand');
|
||||
|
||||
describe('useOrderForm', () => {
|
||||
const marketId = 'market-id';
|
||||
const setup = (marketId: string) => {
|
||||
return renderHook(() => useOrderForm(marketId));
|
||||
};
|
||||
|
||||
it('updates form fields when the order changes', async () => {
|
||||
const order = getDefaultOrder(marketId);
|
||||
const { result } = setup(marketId);
|
||||
// expect default values
|
||||
expect(result.current.order).toEqual(order);
|
||||
expect(result.current.getValues()).toEqual(order);
|
||||
|
||||
const priceUpdate = {
|
||||
...order,
|
||||
price: '100',
|
||||
size: '22',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[marketId]: priceUpdate,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// check order store has updated fields
|
||||
expect(result.current.order).toEqual(priceUpdate);
|
||||
// check react-hook-form has updated fields
|
||||
expect(result.current.getValues()).toEqual(priceUpdate);
|
||||
});
|
||||
|
||||
it('removes persist key on submit', async () => {
|
||||
const order = {
|
||||
...getDefaultOrder(marketId),
|
||||
price: '99',
|
||||
size: '22',
|
||||
};
|
||||
const onSubmit = jest.fn();
|
||||
const { result } = setup(marketId);
|
||||
|
||||
await act(async () => {
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[marketId]: order,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleSubmit(onSubmit)();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit.mock.calls[0][0]).toEqual(omit(order, 'persist'));
|
||||
expect(onSubmit.mock.calls[0][0].persist).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import omit from 'lodash/omit';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import { getDefaultOrder, useOrder } from '@vegaprotocol/orders';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import type { Exact } from 'type-fest';
|
||||
|
||||
export type OrderFormFields = OrderObj & {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects the order store to a react-hook-form instance. Any time a field
|
||||
* changes in the store the form will be updated so that validation rules
|
||||
* for those fields are applied
|
||||
*/
|
||||
export const useOrderForm = (marketId: string) => {
|
||||
const [order, update] = useOrder(marketId);
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitted },
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
clearErrors,
|
||||
getValues,
|
||||
} = useForm<OrderFormFields>({
|
||||
// order can be undefined if there is nothing in the store, it
|
||||
// will be created but the form still needs some default values
|
||||
defaultValues: order || getDefaultOrder(marketId),
|
||||
});
|
||||
|
||||
// Keep form fields in sync with the store values,
|
||||
// inputs are updating the store, fields need updating
|
||||
// to ensure validation rules are applied
|
||||
useEffect(() => {
|
||||
if (!order) return;
|
||||
const currOrder = getValues();
|
||||
for (const k in order) {
|
||||
const key = k as keyof typeof order;
|
||||
const curr = currOrder[key];
|
||||
const value = order[key];
|
||||
if (value !== curr) {
|
||||
setValue(key, value, {
|
||||
shouldValidate: isSubmitted, // only apply validation after the form has been submitted and failed
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [order, isSubmitted, getValues, setValue]);
|
||||
|
||||
const handleSubmitWrapper = (
|
||||
cb: <T>(o: Exact<OrderSubmission, T>) => void
|
||||
) => {
|
||||
return handleSubmit(() => {
|
||||
// remove the persist key from the order in the store, the wallet will reject
|
||||
// an order that contains unrecognized additional keys
|
||||
cb(omit(order, 'persist'));
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
order,
|
||||
update,
|
||||
control,
|
||||
errors,
|
||||
setError,
|
||||
clearErrors,
|
||||
getValues, // returned for test purposes only
|
||||
handleSubmit: handleSubmitWrapper,
|
||||
};
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useOrderMargin } from './use-order-margin';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
|
||||
interface Props {
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
}
|
||||
|
||||
export const useOrderMarginValidation = ({
|
||||
market,
|
||||
marketData,
|
||||
order,
|
||||
}: Props) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const estMargin = useOrderMargin({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
partyId: pubKey || '',
|
||||
});
|
||||
const { id: assetId, decimals: assetDecimals } =
|
||||
market.tradableInstrument.instrument.product.settlementAsset;
|
||||
|
||||
const { accountBalance, accountDecimals } = useAccountBalance(assetId);
|
||||
const balance =
|
||||
accountBalance && accountDecimals !== null
|
||||
? toBigNum(accountBalance, accountDecimals)
|
||||
: toBigNum('0', assetDecimals);
|
||||
const margin = toBigNum(estMargin?.margin || 0, assetDecimals);
|
||||
|
||||
// return only simple types (bool, string) for make memo sensible
|
||||
const balanceError = balance.isGreaterThan(0) && balance.isLessThan(margin);
|
||||
const balanceAsString = balance.toString();
|
||||
const marginAsString = margin.toString();
|
||||
return useMemo(() => {
|
||||
return {
|
||||
balance: balanceAsString,
|
||||
margin: marginAsString,
|
||||
balanceError,
|
||||
};
|
||||
}, [balanceAsString, marginAsString, balanceError]);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import type { PositionMargin } from './use-market-positions';
|
||||
import type { Props } from './use-order-margin';
|
||||
import { useOrderMargin } from './use-order-margin';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
|
||||
let mockEstimateData = {
|
||||
estimateOrder: {
|
||||
fee: {
|
||||
makerFee: '100000.000',
|
||||
infrastructureFee: '100000.000',
|
||||
liquidityFee: '100000.000',
|
||||
},
|
||||
marginLevels: {
|
||||
initialLevel: '200000',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
...jest.requireActual('@apollo/client'),
|
||||
useQuery: jest.fn(() => ({ data: mockEstimateData })),
|
||||
}));
|
||||
|
||||
let mockMarketPositions: PositionMargin = {
|
||||
openVolume: '1',
|
||||
balance: '100000',
|
||||
};
|
||||
|
||||
jest.mock('./use-market-positions', () => ({
|
||||
useMarketPositions: ({
|
||||
marketId,
|
||||
partyId,
|
||||
}: {
|
||||
marketId: string;
|
||||
partyId: string;
|
||||
}) => mockMarketPositions,
|
||||
}));
|
||||
|
||||
describe('useOrderMargin', () => {
|
||||
const marketId = 'marketId';
|
||||
const args: Props = {
|
||||
order: {
|
||||
marketId,
|
||||
size: '2',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
},
|
||||
market: {
|
||||
id: marketId,
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 0,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
} as unknown as Market,
|
||||
marketData: {
|
||||
indicativePrice: '100',
|
||||
markPrice: '200',
|
||||
} as unknown as MarketData,
|
||||
partyId: 'partyId',
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should calculate margin correctly', () => {
|
||||
const { result } = renderHook(() => useOrderMargin(args));
|
||||
expect(result.current?.margin).toEqual('100000');
|
||||
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
|
||||
args.order.size
|
||||
);
|
||||
});
|
||||
|
||||
it('should calculate fees correctly', () => {
|
||||
const { result } = renderHook(() => useOrderMargin(args));
|
||||
expect(result.current?.totalFees).toEqual('300000');
|
||||
});
|
||||
|
||||
it('should not subtract initialMargin if there is no position', () => {
|
||||
mockMarketPositions = null;
|
||||
const { result } = renderHook(() => useOrderMargin(args));
|
||||
expect(result.current?.margin).toEqual('200000');
|
||||
|
||||
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
|
||||
args.order.size
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty value if API fails', () => {
|
||||
mockEstimateData = {
|
||||
estimateOrder: {
|
||||
fee: {
|
||||
makerFee: '100000.000',
|
||||
infrastructureFee: '100000.000',
|
||||
liquidityFee: '100000.000',
|
||||
},
|
||||
marginLevels: {
|
||||
initialLevel: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderHook(() => useOrderMargin(args));
|
||||
expect(result.current).toEqual(null);
|
||||
|
||||
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
|
||||
.plus(args.order.size)
|
||||
.toString();
|
||||
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
|
||||
calledSize
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
import { useMarketPositions } from './use-market-positions';
|
||||
import { useEstimateOrderQuery } from './__generated__/EstimateOrder';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import { getDerivedPrice } from '../utils/get-price';
|
||||
|
||||
export interface Props {
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
partyId: string;
|
||||
derivedPrice?: string;
|
||||
}
|
||||
|
||||
export interface OrderMargin {
|
||||
margin: string;
|
||||
totalFees: string | null;
|
||||
fees: {
|
||||
makerFee: string;
|
||||
liquidityFee: string;
|
||||
infrastructureFee: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const useOrderMargin = ({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
partyId,
|
||||
derivedPrice,
|
||||
}: Props): OrderMargin | null => {
|
||||
const { balance } = useMarketPositions({ marketId: market.id }) || {};
|
||||
const priceForEstimate =
|
||||
derivedPrice || getDerivedPrice(order, market, marketData);
|
||||
|
||||
const { data } = useEstimateOrderQuery({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
partyId,
|
||||
price: priceForEstimate,
|
||||
size: removeDecimal(order.size, market.positionDecimalPlaces),
|
||||
side: order.side,
|
||||
timeInForce: order.timeInForce,
|
||||
type: order.type,
|
||||
},
|
||||
skip: !partyId || !market.id || !order.size || !priceForEstimate,
|
||||
});
|
||||
const { makerFee, liquidityFee, infrastructureFee } = data?.estimateOrder
|
||||
.fee || { makerFee: '', liquidityFee: '', infrastructureFee: '' };
|
||||
const { initialLevel } = data?.estimateOrder.marginLevels ?? {};
|
||||
return useMemo(() => {
|
||||
if (initialLevel) {
|
||||
const margin = BigNumber.maximum(
|
||||
0,
|
||||
new BigNumber(initialLevel).minus(balance || 0)
|
||||
).toString();
|
||||
const fees = new BigNumber(makerFee)
|
||||
.plus(liquidityFee)
|
||||
.plus(infrastructureFee)
|
||||
.toString();
|
||||
return {
|
||||
margin,
|
||||
totalFees: fees,
|
||||
fees: {
|
||||
makerFee,
|
||||
liquidityFee,
|
||||
infrastructureFee,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [initialLevel, makerFee, liquidityFee, infrastructureFee, balance]);
|
||||
};
|
||||
@@ -64,20 +64,27 @@ export function generateMarketData(
|
||||
id: 'market-id',
|
||||
__typename: 'Market',
|
||||
},
|
||||
auctionStart: '2022-06-21T17:18:43.484055236Z',
|
||||
auctionEnd: '2022-06-21T17:18:43.484055236Z',
|
||||
targetStake: '1000000',
|
||||
suppliedStake: '1000',
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
staticMidPrice: '0',
|
||||
indicativePrice: '100',
|
||||
bestStaticBidPrice: '0',
|
||||
bestStaticOfferPrice: '0',
|
||||
indicativeVolume: '10',
|
||||
auctionStart: '2022-06-21T17:18:43.484055236Z',
|
||||
bestBidPrice: '0',
|
||||
bestBidVolume: '0',
|
||||
bestOfferPrice: '0',
|
||||
bestOfferVolume: '0',
|
||||
bestStaticBidPrice: '0',
|
||||
bestStaticBidVolume: '0',
|
||||
bestStaticOfferPrice: '0',
|
||||
bestStaticOfferVolume: '0',
|
||||
indicativePrice: '100',
|
||||
indicativeVolume: '10',
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketValueProxy: '',
|
||||
markPrice: '200',
|
||||
midPrice: '0',
|
||||
openInterest: '',
|
||||
staticMidPrice: '0',
|
||||
suppliedStake: '1000',
|
||||
targetStake: '1000000',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_BATCH,
|
||||
};
|
||||
return merge(defaultMarketData, override);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { isMarketInAuction } from './is-market-in-auction';
|
||||
import type { MarketData, Market } from '@vegaprotocol/market-list';
|
||||
import type { MarketData } from '@vegaprotocol/market-list';
|
||||
|
||||
/**
|
||||
* Get the market price based on market mode (auction or not auction)
|
||||
@@ -34,7 +33,6 @@ export const getDerivedPrice = (
|
||||
type: Schema.OrderType;
|
||||
price?: string | undefined;
|
||||
},
|
||||
market: Market,
|
||||
marketData: MarketData
|
||||
) => {
|
||||
// If order type is market we should use either the mark price
|
||||
@@ -44,7 +42,7 @@ export const getDerivedPrice = (
|
||||
// Use the market price if order is a market order
|
||||
let price;
|
||||
if (order.type === Schema.OrderType.TYPE_LIMIT && order.price) {
|
||||
price = removeDecimal(order.price, market.decimalPlaces);
|
||||
price = order.price;
|
||||
} else {
|
||||
price = getMarketPrice(marketData);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"jest.config.ts"
|
||||
"jest.config.ts",
|
||||
"__mocks__"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
|
||||
interface ApproveNotificationProps {
|
||||
isActive: boolean;
|
||||
selectedAsset?: Asset;
|
||||
onApprove: () => void;
|
||||
approved: boolean;
|
||||
balances: DepositBalances | null;
|
||||
amount: string;
|
||||
approveTxId: number | null;
|
||||
}
|
||||
|
||||
export const ApproveNotification = ({
|
||||
isActive,
|
||||
selectedAsset,
|
||||
onApprove,
|
||||
amount,
|
||||
balances,
|
||||
approved,
|
||||
approveTxId,
|
||||
}: ApproveNotificationProps) => {
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === approveTxId);
|
||||
});
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!selectedAsset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!balances) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const approvePrompt = (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-default"
|
||||
message={t(
|
||||
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
|
||||
)}
|
||||
buttonProps={{
|
||||
size: 'sm',
|
||||
text: `Approve ${selectedAsset?.symbol}`,
|
||||
action: onApprove,
|
||||
dataTestId: 'approve-submit',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const reApprovePrompt = (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="reapprove-default"
|
||||
message={t(
|
||||
`Approve again to deposit more than ${formatNumber(
|
||||
balances.allowance.toString()
|
||||
)}`
|
||||
)}
|
||||
buttonProps={{
|
||||
size: 'sm',
|
||||
text: `Approve ${selectedAsset?.symbol}`,
|
||||
action: onApprove,
|
||||
dataTestId: 'reapprove-submit',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const approvalFeedback = (
|
||||
<ApprovalTxFeedback
|
||||
tx={tx}
|
||||
selectedAsset={selectedAsset}
|
||||
allowance={balances.allowance}
|
||||
/>
|
||||
);
|
||||
|
||||
// always show requested and pending states
|
||||
if (
|
||||
tx &&
|
||||
[EthTxStatus.Requested, EthTxStatus.Pending, EthTxStatus.Complete].includes(
|
||||
tx.status
|
||||
)
|
||||
) {
|
||||
return approvalFeedback;
|
||||
}
|
||||
|
||||
if (!approved) {
|
||||
return approvePrompt;
|
||||
}
|
||||
|
||||
if (new BigNumber(amount).isGreaterThan(balances.allowance)) {
|
||||
return reApprovePrompt;
|
||||
}
|
||||
|
||||
if (
|
||||
tx &&
|
||||
tx.status === EthTxStatus.Error &&
|
||||
// @ts-ignore tx.error not typed correctly
|
||||
tx.error.code === 'ACTION_REJECTED'
|
||||
) {
|
||||
return approvePrompt;
|
||||
}
|
||||
|
||||
return approvalFeedback;
|
||||
};
|
||||
|
||||
const ApprovalTxFeedback = ({
|
||||
tx,
|
||||
selectedAsset,
|
||||
allowance,
|
||||
}: {
|
||||
tx: EthStoredTxState | undefined;
|
||||
selectedAsset: Asset;
|
||||
allowance?: BigNumber;
|
||||
}) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
|
||||
if (!tx) return null;
|
||||
|
||||
const txLink = tx.txHash && (
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
);
|
||||
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Danger}
|
||||
testId="approve-error"
|
||||
message={
|
||||
<p>
|
||||
{t('Approval failed')} {txLink}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Requested) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-requested"
|
||||
message={t(
|
||||
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Pending) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Primary}
|
||||
testId="approve-pending"
|
||||
message={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
`Your ${selectedAsset?.symbol} is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
|
||||
)}{' '}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Confirmed) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Success}
|
||||
testId="approve-confirmed"
|
||||
message={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
`You can now make deposits in ${
|
||||
selectedAsset?.symbol
|
||||
}, up to a maximum of ${formatNumber(
|
||||
allowance?.toString() || 0
|
||||
)}`
|
||||
)}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -4,21 +4,15 @@ import { DepositManager } from './deposit-manager';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { enabledAssetsProvider } from '@vegaprotocol/assets';
|
||||
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
|
||||
|
||||
/**
|
||||
* Fetches data required for the Deposit page
|
||||
*/
|
||||
export const DepositContainer = ({
|
||||
assetId,
|
||||
setDialogStyleProps,
|
||||
}: {
|
||||
assetId?: string;
|
||||
setDialogStyleProps?: DepositDialogStylePropsSetter;
|
||||
}) => {
|
||||
export const DepositContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: enabledAssetsProvider,
|
||||
variables: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -28,7 +22,6 @@ export const DepositContainer = ({
|
||||
assetId={assetId}
|
||||
assets={data}
|
||||
isFaucetable={VEGA_ENV !== Networks.MAINNET}
|
||||
setDialogStyleProps={setDialogStyleProps}
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DepositContainer } from './deposit-container';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
@@ -24,22 +22,6 @@ export const useDepositDialog = create<State & Actions>((set) => ({
|
||||
close: () => set(() => ({ assetId: undefined, isOpen: false })),
|
||||
}));
|
||||
|
||||
export type DepositDialogStyleProps = {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
intent?: Intent;
|
||||
};
|
||||
|
||||
export type DepositDialogStylePropsSetter = (
|
||||
props?: DepositDialogStyleProps
|
||||
) => void;
|
||||
|
||||
const DEFAULT_STYLE: DepositDialogStyleProps = {
|
||||
title: t('Deposit'),
|
||||
intent: undefined,
|
||||
icon: undefined,
|
||||
};
|
||||
|
||||
export const DepositDialog = () => {
|
||||
const { assetId, isOpen, open, close } = useDepositDialog();
|
||||
const assetDetailsDialogOpen = useAssetDetailsDialogStore(
|
||||
@@ -48,25 +30,13 @@ export const DepositDialog = () => {
|
||||
const connectWalletDialogIsOpen = useWeb3ConnectStore(
|
||||
(state) => state.isOpen
|
||||
);
|
||||
const [dialogStyleProps, _setDialogStyleProps] = useState(DEFAULT_STYLE);
|
||||
const setDialogStyleProps: DepositDialogStylePropsSetter =
|
||||
useCallback<DepositDialogStylePropsSetter>(
|
||||
(props) =>
|
||||
props
|
||||
? _setDialogStyleProps(props)
|
||||
: _setDialogStyleProps(DEFAULT_STYLE),
|
||||
[_setDialogStyleProps]
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen && !(connectWalletDialogIsOpen || assetDetailsDialogOpen)}
|
||||
onChange={(isOpen) => (isOpen ? open() : close())}
|
||||
{...dialogStyleProps}
|
||||
title={t('Deposit')}
|
||||
>
|
||||
<DepositContainer
|
||||
assetId={assetId}
|
||||
setDialogStyleProps={setDialogStyleProps}
|
||||
/>
|
||||
<DepositContainer assetId={assetId} />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
jest.mock('@vegaprotocol/web3');
|
||||
@@ -38,27 +39,34 @@ function generateAsset(): AssetFieldsFragment {
|
||||
|
||||
let asset: AssetFieldsFragment;
|
||||
let props: DepositFormProps;
|
||||
let balances: DepositBalances;
|
||||
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
const MOCK_VEGA_KEY =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
|
||||
beforeEach(() => {
|
||||
asset = generateAsset();
|
||||
balances = {
|
||||
balance: new BigNumber(5),
|
||||
max: new BigNumber(20),
|
||||
allowance: new BigNumber(30),
|
||||
deposited: new BigNumber(10),
|
||||
};
|
||||
props = {
|
||||
assets: [asset],
|
||||
selectedAsset: undefined,
|
||||
onSelectAsset: jest.fn(),
|
||||
balance: new BigNumber(5),
|
||||
balances,
|
||||
submitApprove: jest.fn(),
|
||||
submitDeposit: jest.fn(),
|
||||
requestFaucet: jest.fn(),
|
||||
max: new BigNumber(20),
|
||||
deposited: new BigNumber(10),
|
||||
allowance: new BigNumber(30),
|
||||
submitFaucet: jest.fn(),
|
||||
onDisconnect: jest.fn(),
|
||||
approveTxId: null,
|
||||
faucetTxId: null,
|
||||
isFaucetable: true,
|
||||
};
|
||||
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null });
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null, pubKeys: [] });
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: true,
|
||||
account: MOCK_ETH_ADDRESS,
|
||||
@@ -71,7 +79,8 @@ describe('Deposit form', () => {
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
|
||||
expect(screen.getByLabelText('From (Ethereum address)')).toHaveValue(
|
||||
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
|
||||
MOCK_ETH_ADDRESS
|
||||
);
|
||||
expect(screen.getByLabelText('Asset')).toHaveValue('');
|
||||
@@ -145,7 +154,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above deposit limit')
|
||||
await screen.findByText('Amount is above lifetime deposit limit')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -153,9 +162,12 @@ describe('Deposit form', () => {
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
balance={new BigNumber(100)}
|
||||
max={new BigNumber(100)}
|
||||
deposited={new BigNumber(10)}
|
||||
balances={{
|
||||
...balances,
|
||||
balance: BigNumber(100),
|
||||
max: new BigNumber(100),
|
||||
deposited: new BigNumber(10),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -166,7 +178,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above approved amount.')
|
||||
await screen.findByText('Amount is above approved amount')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -214,14 +226,17 @@ describe('Deposit form', () => {
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
allowance={new BigNumber(0)}
|
||||
balances={{
|
||||
...balances,
|
||||
allowance: new BigNumber(0),
|
||||
}}
|
||||
selectedAsset={asset}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
|
||||
`Deposits of ${asset.symbol} not approved`
|
||||
expect(screen.getByTestId('approve-default')).toHaveTextContent(
|
||||
`Before you can make a deposit of your chosen asset, ${asset.symbol}, you need to approve its use in your Ethereum wallet`
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
@@ -253,10 +268,12 @@ describe('Deposit form', () => {
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
allowance={new BigNumber(100)}
|
||||
balance={balance}
|
||||
max={max}
|
||||
deposited={deposited}
|
||||
balances={{
|
||||
allowance: new BigNumber(100),
|
||||
balance,
|
||||
max,
|
||||
deposited,
|
||||
}}
|
||||
selectedAsset={asset}
|
||||
/>
|
||||
);
|
||||
@@ -320,10 +337,10 @@ describe('Deposit form', () => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Connect' })
|
||||
).not.toBeInTheDocument();
|
||||
const fromInput = screen.getByLabelText('From (Ethereum address)');
|
||||
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
|
||||
expect(fromInput).toBeDisabled();
|
||||
expect(fromInput).toHaveAttribute('readonly');
|
||||
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
|
||||
MOCK_ETH_ADDRESS
|
||||
);
|
||||
});
|
||||
|
||||
it('prevents submission if you are on the wrong chain', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import type { Asset, AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { AssetOption } from '@vegaprotocol/assets';
|
||||
import {
|
||||
ethereumAddress,
|
||||
@@ -19,13 +19,16 @@ import {
|
||||
RichSelect,
|
||||
Notification,
|
||||
Intent,
|
||||
ButtonLink,
|
||||
Select,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { FieldError } from 'react-hook-form';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { DepositLimits } from './deposit-limits';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
@@ -34,6 +37,9 @@ import {
|
||||
useWeb3ConnectStore,
|
||||
getChainName,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
import { FaucetNotification } from './faucet-notification';
|
||||
import { ApproveNotification } from './approve-notification';
|
||||
|
||||
interface FormFields {
|
||||
asset: string;
|
||||
@@ -45,38 +51,38 @@ interface FormFields {
|
||||
export interface DepositFormProps {
|
||||
assets: Asset[];
|
||||
selectedAsset?: Asset;
|
||||
balances: DepositBalances | null;
|
||||
onSelectAsset: (assetId: string) => void;
|
||||
balance: BigNumber | undefined;
|
||||
onDisconnect: () => void;
|
||||
submitApprove: () => void;
|
||||
approveTxId: number | null;
|
||||
submitFaucet: () => void;
|
||||
faucetTxId: number | null;
|
||||
submitDeposit: (args: {
|
||||
assetSource: string;
|
||||
amount: string;
|
||||
vegaPublicKey: string;
|
||||
}) => void;
|
||||
requestFaucet: () => void;
|
||||
max: BigNumber | undefined;
|
||||
deposited: BigNumber | undefined;
|
||||
allowance: BigNumber | undefined;
|
||||
isFaucetable?: boolean;
|
||||
}
|
||||
|
||||
export const DepositForm = ({
|
||||
assets,
|
||||
selectedAsset,
|
||||
balances,
|
||||
onSelectAsset,
|
||||
balance,
|
||||
max,
|
||||
deposited,
|
||||
onDisconnect,
|
||||
submitApprove,
|
||||
submitDeposit,
|
||||
requestFaucet,
|
||||
allowance,
|
||||
submitFaucet,
|
||||
faucetTxId,
|
||||
approveTxId,
|
||||
isFaucetable,
|
||||
}: DepositFormProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const openDialog = useWeb3ConnectStore((store) => store.open);
|
||||
const { isActive, account } = useWeb3React();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -91,43 +97,21 @@ export const DepositForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const amount = useWatch({ name: 'amount', control });
|
||||
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
|
||||
throw new Error('Invalid asset');
|
||||
}
|
||||
if (!approved) throw new Error('Deposits not approved');
|
||||
|
||||
if (approved) {
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
} else {
|
||||
submitApprove();
|
||||
}
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
};
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
const maxApproved = allowance ? allowance : 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
|
||||
let maxLimit = new BigNumber(Infinity);
|
||||
|
||||
// A max limit of zero indicates that there is no limit
|
||||
if (max && deposited && max.isGreaterThan(0)) {
|
||||
maxLimit = max.minus(deposited);
|
||||
}
|
||||
|
||||
return {
|
||||
approved: maxApproved,
|
||||
available: maxAvailable,
|
||||
limit: maxLimit,
|
||||
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
|
||||
};
|
||||
}, [max, deposited, allowance, balance]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const minViableAmount = selectedAsset
|
||||
@@ -137,8 +121,15 @@ export const DepositForm = ({
|
||||
return minViableAmount;
|
||||
}, [selectedAsset]);
|
||||
|
||||
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
|
||||
const formState = getFormState(selectedAsset, isActive, approved);
|
||||
const pubKeys = useMemo(() => {
|
||||
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
|
||||
}, [_pubKeys]);
|
||||
|
||||
const approved = balances
|
||||
? balances.allowance.isGreaterThan(0)
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -166,32 +157,23 @@ export const DepositForm = ({
|
||||
render={() => {
|
||||
if (isActive && account) {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
value={account}
|
||||
readOnly={true}
|
||||
disabled={true}
|
||||
{...register('from', {
|
||||
validate: {
|
||||
required,
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<div className="text-sm" aria-describedby="ethereum-address">
|
||||
<p className="mb-1" data-testid="ethereum-address">
|
||||
{account}
|
||||
</p>
|
||||
<DisconnectEthereumButton
|
||||
onDisconnect={() => {
|
||||
setValue('from', ''); // clear from value so required ethereum connection validation works
|
||||
onDisconnect();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onClick={openDialog}
|
||||
variant="primary"
|
||||
fill={true}
|
||||
type="button"
|
||||
data-testid="connect-eth-wallet-btn"
|
||||
>
|
||||
@@ -238,7 +220,7 @@ export const DepositForm = ({
|
||||
</InputError>
|
||||
)}
|
||||
{isFaucetable && selectedAsset && (
|
||||
<UseButton onClick={requestFaucet}>
|
||||
<UseButton onClick={submitFaucet}>
|
||||
{t(`Get ${selectedAsset.symbol}`)}
|
||||
</UseButton>
|
||||
)}
|
||||
@@ -255,39 +237,55 @@ export const DepositForm = ({
|
||||
</button>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FaucetNotification
|
||||
isActive={isActive}
|
||||
selectedAsset={selectedAsset}
|
||||
faucetTxId={faucetTxId}
|
||||
/>
|
||||
<FormGroup label={t('To (Vega key)')} labelFor="to">
|
||||
<Input
|
||||
{...register('to', { validate: { required, vegaPublicKey } })}
|
||||
id="to"
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('to', '')}
|
||||
select={
|
||||
<Select {...register('to')} id="to" defaultValue="">
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.length &&
|
||||
pubKeys.map((pk) => (
|
||||
<option key={pk} value={pk}>
|
||||
{pk}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
input={
|
||||
<Input
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="to"
|
||||
type="text"
|
||||
{...register('to', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.to?.message && (
|
||||
<InputError intent="danger" forInput="to">
|
||||
{errors.to.message}
|
||||
</InputError>
|
||||
)}
|
||||
{pubKey && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('to', pubKey);
|
||||
clearErrors('to');
|
||||
}}
|
||||
>
|
||||
{t('Use connected')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
{selectedAsset && max && deposited && (
|
||||
{selectedAsset && balances && (
|
||||
<div className="mb-6">
|
||||
<DepositLimits
|
||||
max={max}
|
||||
deposited={deposited}
|
||||
balance={balance}
|
||||
asset={selectedAsset}
|
||||
allowance={allowance}
|
||||
/>
|
||||
<DepositLimits {...balances} asset={selectedAsset} />
|
||||
</div>
|
||||
)}
|
||||
{formState === 'deposit' && (
|
||||
{approved && (
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -299,38 +297,52 @@ export const DepositForm = ({
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
approved: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.approved)) {
|
||||
if (value.isGreaterThan(balances?.allowance || 0)) {
|
||||
return t('Amount is above approved amount');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
limit: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.limit)) {
|
||||
return t('Amount is above deposit limit');
|
||||
if (!balances) {
|
||||
return t('Could not verify balances of account'); // this should never happen
|
||||
}
|
||||
|
||||
let lifetimeLimit = new BigNumber(Infinity);
|
||||
if (balances.max.isGreaterThan(0)) {
|
||||
lifetimeLimit = balances.max.minus(balances.deposited);
|
||||
}
|
||||
|
||||
if (value.isGreaterThan(lifetimeLimit)) {
|
||||
return t('Amount is above lifetime deposit limit');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
balance: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
if (value.isGreaterThan(balances?.balance || 0)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
return maxSafe(maxAmount.amount)(v);
|
||||
return maxSafe(balances?.balance || new BigNumber(0))(v);
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<AmountError error={errors.amount} submitApprove={submitApprove} />
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{errors.amount.message}
|
||||
</InputError>
|
||||
)}
|
||||
{selectedAsset && balance && (
|
||||
{selectedAsset && balances && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
setValue(
|
||||
'amount',
|
||||
balances.balance.toFixed(selectedAsset.decimals)
|
||||
);
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
@@ -339,59 +351,31 @@ export const DepositForm = ({
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormButton selectedAsset={selectedAsset} formState={formState} />
|
||||
<ApproveNotification
|
||||
isActive={isActive}
|
||||
approveTxId={approveTxId}
|
||||
selectedAsset={selectedAsset}
|
||||
onApprove={submitApprove}
|
||||
balances={balances}
|
||||
approved={approved}
|
||||
amount={amount}
|
||||
/>
|
||||
<FormButton approved={approved} selectedAsset={selectedAsset} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const AmountError = ({
|
||||
error,
|
||||
submitApprove,
|
||||
}: {
|
||||
error: FieldError;
|
||||
submitApprove: () => void;
|
||||
}) => {
|
||||
if (error.type === 'approved') {
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}.
|
||||
<button onClick={submitApprove} className="underline ml-2">
|
||||
{t('Update approve amount')}
|
||||
</button>
|
||||
</InputError>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}
|
||||
</InputError>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormButtonProps {
|
||||
selectedAsset?: Asset;
|
||||
formState: ReturnType<typeof getFormState>;
|
||||
approved: boolean;
|
||||
selectedAsset: AssetFieldsFragment | undefined;
|
||||
}
|
||||
|
||||
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
||||
const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
|
||||
const { isActive, chainId } = useWeb3React();
|
||||
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
||||
const submitText =
|
||||
formState === 'approve'
|
||||
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
|
||||
: t('Deposit');
|
||||
const invalidChain = isActive && chainId !== desiredChainId;
|
||||
return (
|
||||
<>
|
||||
{formState === 'approve' && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-warning"
|
||||
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{invalidChain && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
@@ -408,9 +392,9 @@ const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
||||
data-testid="deposit-submit"
|
||||
variant={isActive ? 'primary' : 'default'}
|
||||
fill={true}
|
||||
disabled={invalidChain}
|
||||
disabled={invalidChain || (selectedAsset && !approved)}
|
||||
>
|
||||
{submitText}
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
@@ -437,7 +421,7 @@ const DisconnectEthereumButton = ({
|
||||
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
||||
|
||||
return (
|
||||
<UseButton
|
||||
<ButtonLink
|
||||
onClick={() => {
|
||||
connector.deactivate();
|
||||
removeEagerConnector();
|
||||
@@ -446,17 +430,46 @@ const DisconnectEthereumButton = ({
|
||||
data-testid="disconnect-ethereum-wallet"
|
||||
>
|
||||
{t('Disconnect')}
|
||||
</UseButton>
|
||||
</ButtonLink>
|
||||
);
|
||||
};
|
||||
|
||||
const getFormState = (
|
||||
selectedAsset: Asset | undefined,
|
||||
isActive: boolean,
|
||||
approved: boolean
|
||||
) => {
|
||||
if (!selectedAsset) return 'deposit';
|
||||
if (!isActive) return 'deposit';
|
||||
if (approved) return 'deposit';
|
||||
return 'approve';
|
||||
interface AddressInputProps {
|
||||
pubKeys: string[] | null;
|
||||
select: ReactNode;
|
||||
input: ReactNode;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const AddressField = ({
|
||||
pubKeys,
|
||||
select,
|
||||
input,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const [isInput, setIsInput] = useState(() => {
|
||||
if (pubKeys && pubKeys.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInput ? input : select}
|
||||
{pubKeys && pubKeys.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="ml-auto text-sm absolute top-0 right-0 underline"
|
||||
data-testid="enter-pubkey-manually"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DepositLimits = ({
|
||||
},
|
||||
{
|
||||
key: 'MAX_LIMIT',
|
||||
label: t('Maximum total deposit amount'),
|
||||
label: t('Lifetime deposit allowance'),
|
||||
rawValue: max,
|
||||
value: <CompactNumber number={max} decimals={asset.decimals} />,
|
||||
},
|
||||
|
||||
@@ -5,36 +5,26 @@ import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useSubmitApproval } from './use-submit-approval';
|
||||
import { useSubmitFaucet } from './use-submit-faucet';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useDepositBalances } from './use-deposit-balances';
|
||||
import { useDepositDialog } from './deposit-dialog';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
|
||||
import pick from 'lodash/pick';
|
||||
import type { EthTransaction } from '@vegaprotocol/web3';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthTransactionStore,
|
||||
useBridgeContract,
|
||||
useEthereumConfig,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
interface DepositManagerProps {
|
||||
assetId?: string;
|
||||
assets: Asset[];
|
||||
isFaucetable: boolean;
|
||||
setDialogStyleProps?: DepositDialogStylePropsSetter;
|
||||
}
|
||||
|
||||
const getProps = (txContent?: EthTransaction['TxContent']) =>
|
||||
txContent ? pick(txContent, ['title', 'icon', 'intent']) : undefined;
|
||||
|
||||
export const DepositManager = ({
|
||||
assetId: initialAssetId,
|
||||
assets,
|
||||
isFaucetable,
|
||||
setDialogStyleProps,
|
||||
}: DepositManagerProps) => {
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const { config } = useEthereumConfig();
|
||||
@@ -43,26 +33,16 @@ export const DepositManager = ({
|
||||
const bridgeContract = useBridgeContract();
|
||||
const closeDepositDialog = useDepositDialog((state) => state.close);
|
||||
|
||||
const { balance, allowance, deposited, max, refresh } = useDepositBalances(
|
||||
const { getBalances, reset, balances } = useDepositBalances(
|
||||
asset,
|
||||
isFaucetable
|
||||
);
|
||||
|
||||
// Set up approve transaction
|
||||
const approve = useSubmitApproval(asset);
|
||||
const approve = useSubmitApproval(asset, getBalances);
|
||||
|
||||
// Set up faucet transaction
|
||||
const faucet = useSubmitFaucet(asset);
|
||||
|
||||
const transactionInProgress = [approve.TxContent, faucet.TxContent].filter(
|
||||
(t) => t.status !== EthTxStatus.Default
|
||||
)[0];
|
||||
|
||||
useEffect(() => {
|
||||
setDialogStyleProps?.(getProps(transactionInProgress));
|
||||
}, [setDialogStyleProps, transactionInProgress]);
|
||||
|
||||
const returnLabel = t('Return to deposit');
|
||||
const faucet = useSubmitFaucet(asset, getBalances);
|
||||
|
||||
const submitDeposit = (
|
||||
args: Parameters<DepositFormProps['submitDeposit']>['0']
|
||||
@@ -86,31 +66,24 @@ export const DepositManager = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!transactionInProgress && (
|
||||
<DepositForm
|
||||
balance={balance}
|
||||
selectedAsset={asset}
|
||||
onSelectAsset={setAssetId}
|
||||
assets={sortBy(assets, 'name')}
|
||||
submitApprove={async () => {
|
||||
await approve.perform();
|
||||
refresh();
|
||||
}}
|
||||
submitDeposit={submitDeposit}
|
||||
requestFaucet={async () => {
|
||||
await faucet.perform();
|
||||
refresh();
|
||||
}}
|
||||
deposited={deposited}
|
||||
max={max}
|
||||
allowance={allowance}
|
||||
isFaucetable={isFaucetable}
|
||||
/>
|
||||
)}
|
||||
|
||||
<approve.TxContent.Content returnLabel={returnLabel} />
|
||||
<faucet.TxContent.Content returnLabel={returnLabel} />
|
||||
</>
|
||||
<DepositForm
|
||||
selectedAsset={asset}
|
||||
onDisconnect={reset}
|
||||
onSelectAsset={(id) => {
|
||||
setAssetId(id);
|
||||
// When we change asset, also clear the tracked faucet/approve transactions so
|
||||
// we dont render stale UI
|
||||
approve.reset();
|
||||
faucet.reset();
|
||||
}}
|
||||
assets={sortBy(assets, 'name')}
|
||||
submitApprove={approve.perform}
|
||||
submitDeposit={submitDeposit}
|
||||
submitFaucet={faucet.perform}
|
||||
faucetTxId={faucet.id}
|
||||
approveTxId={approve.id}
|
||||
balances={balances}
|
||||
isFaucetable={isFaucetable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -72,7 +72,9 @@ export const DepositsTable = forwardRef<
|
||||
field="txHash"
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<DepositFieldsFragment, 'txHash'>) => {
|
||||
if (!data) return null;
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
|
||||
interface FaucetNotificationProps {
|
||||
isActive: boolean;
|
||||
selectedAsset?: Asset;
|
||||
faucetTxId: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a notification for the faucet transaction
|
||||
*/
|
||||
export const FaucetNotification = ({
|
||||
isActive,
|
||||
selectedAsset,
|
||||
faucetTxId,
|
||||
}: FaucetNotificationProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === faucetTxId);
|
||||
});
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!selectedAsset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Danger}
|
||||
testId="faucet-error"
|
||||
// @ts-ignore tx.error not typed correctly
|
||||
message={t(`Faucet failed: ${tx.error?.reason}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Requested) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="faucet-requested"
|
||||
message={t(
|
||||
`Go to your Ethereum wallet and approve the faucet transaction for ${selectedAsset?.symbol}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Pending) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Primary}
|
||||
testId="faucet-pending"
|
||||
message={
|
||||
<p>
|
||||
{t('Faucet pending...')}{' '}
|
||||
{tx.txHash && (
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Confirmed) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Success}
|
||||
testId="faucet-confirmed"
|
||||
message={
|
||||
<p>
|
||||
{t('Faucet successful')}{' '}
|
||||
{tx.txHash && (
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -7,21 +7,17 @@ import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
|
||||
import { useGetDepositMaximum } from './use-get-deposit-maximum';
|
||||
import { useGetDepositedAmount } from './use-get-deposited-amount';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||
import { usePrevious } from '@vegaprotocol/react-helpers';
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
|
||||
type DepositBalances = {
|
||||
balance: BigNumber;
|
||||
allowance: BigNumber;
|
||||
deposited: BigNumber;
|
||||
max: BigNumber;
|
||||
refresh: () => void;
|
||||
};
|
||||
export interface DepositBalances {
|
||||
balance: BigNumber; // amount in Ethereum wallet
|
||||
allowance: BigNumber; // amount approved
|
||||
deposited: BigNumber; // total amounted deposited over lifetime
|
||||
max: BigNumber; // life time deposit cap
|
||||
}
|
||||
|
||||
type DepositBalancesState = Omit<DepositBalances, 'refresh'>;
|
||||
|
||||
const initialState: DepositBalancesState = {
|
||||
const initialState: DepositBalances = {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
deposited: new BigNumber(0),
|
||||
@@ -35,7 +31,7 @@ const initialState: DepositBalancesState = {
|
||||
export const useDepositBalances = (
|
||||
asset: Asset | undefined,
|
||||
isFaucetable: boolean
|
||||
): DepositBalances => {
|
||||
) => {
|
||||
const tokenContract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
isFaucetable
|
||||
@@ -45,21 +41,14 @@ export const useDepositBalances = (
|
||||
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
|
||||
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
|
||||
const getDepositedAmount = useGetDepositedAmount(asset);
|
||||
const prevAsset = usePrevious(asset);
|
||||
const [state, setState] = useState<DepositBalancesState>(initialState);
|
||||
|
||||
useEffect(() => {
|
||||
if (asset?.id !== prevAsset?.id) {
|
||||
// reset values to initial state when asset changes
|
||||
setState(initialState);
|
||||
}
|
||||
}, [asset?.id, prevAsset?.id]);
|
||||
const [state, setState] = useState<DepositBalances | null>(null);
|
||||
|
||||
const { accountBalance } = useAccountBalance(asset?.id);
|
||||
|
||||
const getBalances = useCallback(async () => {
|
||||
if (!asset) return;
|
||||
try {
|
||||
setState(null);
|
||||
const [max, deposited, balance, allowance] = await Promise.all([
|
||||
getDepositMaximum(),
|
||||
getDepositedAmount(),
|
||||
@@ -75,12 +64,17 @@ export const useDepositBalances = (
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
setState(null);
|
||||
}
|
||||
}, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getBalances();
|
||||
}, [asset, getBalances, accountBalance]);
|
||||
|
||||
return { ...state, refresh: getBalances };
|
||||
return { balances: state, getBalances, reset };
|
||||
};
|
||||
|
||||
@@ -1,36 +1,48 @@
|
||||
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { Token } from '@vegaprotocol/smart-contracts';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthereumConfig,
|
||||
useEthereumTransaction,
|
||||
useEthTransactionStore,
|
||||
useTokenContract,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useSubmitApproval = (asset?: Asset) => {
|
||||
export const useSubmitApproval = (
|
||||
asset: Asset | undefined,
|
||||
getBalances: () => void
|
||||
) => {
|
||||
const [id, setId] = useState<number | null>(null);
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === id);
|
||||
});
|
||||
const { config } = useEthereumConfig();
|
||||
const contract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
true
|
||||
);
|
||||
const transaction = useEthereumTransaction<Token, 'approve'>(
|
||||
contract,
|
||||
'approve'
|
||||
);
|
||||
|
||||
// When tx is confirmed refresh balances
|
||||
useEffect(() => {
|
||||
if (tx?.status === EthTxStatus.Confirmed) {
|
||||
getBalances();
|
||||
}
|
||||
}, [tx?.status, getBalances]);
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
perform: async () => {
|
||||
id,
|
||||
reset: () => {
|
||||
setId(null);
|
||||
},
|
||||
perform: () => {
|
||||
if (!asset || !config) return;
|
||||
try {
|
||||
const amount = removeDecimal('1000000', asset.decimals);
|
||||
await transaction.perform(
|
||||
config.collateral_bridge_contract.address,
|
||||
amount
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
const amount = removeDecimal('1000000', asset.decimals);
|
||||
const id = createEthTransaction(contract, 'approve', [
|
||||
config?.collateral_bridge_contract.address,
|
||||
amount,
|
||||
]);
|
||||
setId(id);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthTransactionStore,
|
||||
useTokenContract,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useSubmitFaucet = (asset?: Asset) => {
|
||||
export const useSubmitFaucet = (
|
||||
asset: Asset | undefined,
|
||||
getBalances: () => void
|
||||
) => {
|
||||
const [id, setId] = useState<number | null>(null);
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === id);
|
||||
});
|
||||
const contract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
true
|
||||
);
|
||||
const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>(
|
||||
contract,
|
||||
'faucet'
|
||||
);
|
||||
|
||||
// When tx is confirmed refresh balances
|
||||
useEffect(() => {
|
||||
if (tx?.status === EthTxStatus.Confirmed) {
|
||||
getBalances();
|
||||
}
|
||||
}, [tx?.status, getBalances]);
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
perform: async () => {
|
||||
try {
|
||||
await transaction.perform();
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
id,
|
||||
reset: () => {
|
||||
setId(null);
|
||||
},
|
||||
perform: () => {
|
||||
const id = createEthTransaction(contract, 'faucet', []);
|
||||
setId(id);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts",
|
||||
"**/__mocks__/*.tsx",
|
||||
"jest.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { PageInfo, Edge } from '@vegaprotocol/utils';
|
||||
import { FillsDocument, FillsEventDocument } from './__generated__/Fills';
|
||||
import type {
|
||||
FillsQuery,
|
||||
FillsQueryVariables,
|
||||
FillFieldsFragment,
|
||||
FillEdgeFragment,
|
||||
FillsEventSubscription,
|
||||
@@ -56,19 +57,28 @@ const update = (
|
||||
});
|
||||
};
|
||||
|
||||
export type Trade = Omit<FillFieldsFragment, 'market'> & { market?: Market };
|
||||
export type Trade = Omit<FillFieldsFragment, 'market'> & {
|
||||
market?: Market;
|
||||
isLastPlaceholder?: boolean;
|
||||
};
|
||||
export type TradeEdge = Edge<Trade>;
|
||||
|
||||
const getData = (responseData: FillsQuery | null): FillEdgeFragment[] =>
|
||||
responseData?.party?.tradesConnection?.edges || [];
|
||||
|
||||
const getPageInfo = (responseData: FillsQuery): PageInfo | null =>
|
||||
responseData.party?.tradesConnection?.pageInfo || null;
|
||||
const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
|
||||
responseData?.party?.tradesConnection?.pageInfo || null;
|
||||
|
||||
const getDelta = (subscriptionData: FillsEventSubscription) =>
|
||||
subscriptionData.trades || [];
|
||||
|
||||
export const fillsProvider = makeDataProvider({
|
||||
export const fillsProvider = makeDataProvider<
|
||||
Parameters<typeof getData>['0'],
|
||||
ReturnType<typeof getData>,
|
||||
Parameters<typeof getDelta>['0'],
|
||||
ReturnType<typeof getDelta>,
|
||||
FillsQueryVariables
|
||||
>({
|
||||
query: FillsDocument,
|
||||
subscriptionQuery: FillsEventDocument,
|
||||
update,
|
||||
@@ -83,9 +93,13 @@ export const fillsProvider = makeDataProvider({
|
||||
|
||||
export const fillsWithMarketProvider = makeDerivedDataProvider<
|
||||
(TradeEdge | null)[],
|
||||
Trade[]
|
||||
Trade[],
|
||||
FillsQueryVariables
|
||||
>(
|
||||
[fillsProvider, marketsProvider],
|
||||
[
|
||||
fillsProvider,
|
||||
(callback, client) => marketsProvider(callback, client, undefined),
|
||||
],
|
||||
(partsData): (TradeEdge | null)[] =>
|
||||
(partsData[0] as ReturnType<typeof getData>)?.map(
|
||||
(edge) =>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FillsTable } from './fills-table';
|
||||
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
||||
import { useFillsList } from './use-fills-list';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
@@ -19,22 +21,51 @@ export const FillsManager = ({
|
||||
}: FillsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const scrolledToTop = useRef(true);
|
||||
const { data, error, loading, addNewRows, getRows, reload } = useFillsList({
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
addNewRows,
|
||||
getRows,
|
||||
reload,
|
||||
makeBottomPlaceholders,
|
||||
} = useFillsList({
|
||||
partyId,
|
||||
marketId,
|
||||
gridRef,
|
||||
scrolledToTop,
|
||||
});
|
||||
|
||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||
if (event.top === 0) {
|
||||
addNewRows();
|
||||
const checkBottomPlaceholder = useCallback(() => {
|
||||
const rowCont = gridRef.current?.api?.getModel().getRowCount() ?? 0;
|
||||
const lastRowIndex = gridRef.current?.api?.getLastDisplayedRow();
|
||||
if (lastRowIndex && rowCont - 1 === lastRowIndex) {
|
||||
const lastrow = gridRef.current?.api.getDisplayedRowAtIndex(lastRowIndex);
|
||||
lastrow?.setRowHeight(50);
|
||||
makeBottomPlaceholders(lastrow?.data);
|
||||
gridRef.current?.api.onRowHeightChanged();
|
||||
gridRef.current?.api.refreshInfiniteCache();
|
||||
}
|
||||
};
|
||||
}, [makeBottomPlaceholders]);
|
||||
|
||||
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||
const onBodyScrollEnd = useCallback(
|
||||
(event: BodyScrollEndEvent) => {
|
||||
if (event.top === 0) {
|
||||
addNewRows();
|
||||
}
|
||||
checkBottomPlaceholder();
|
||||
},
|
||||
[addNewRows, checkBottomPlaceholder]
|
||||
);
|
||||
|
||||
const onBodyScroll = useCallback((event: BodyScrollEvent) => {
|
||||
scrolledToTop.current = event.top <= 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { isFullWidthRow, fullWidthCellRenderer, rowClassRules } =
|
||||
useBottomPlaceholder<Trade>({
|
||||
gridRef,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
@@ -48,6 +79,9 @@ export const FillsManager = ({
|
||||
onMarketClick={onMarketClick}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
isFullWidthRow={isFullWidthRow}
|
||||
fullWidthCellRenderer={fullWidthCellRenderer}
|
||||
rowClassRules={rowClassRules}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Schema from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
|
||||
import { FillsTable } from './fills-table';
|
||||
import { FillsTable, getFeesBreakdown } from './fills-table';
|
||||
import { generateFill } from './test-helpers';
|
||||
|
||||
describe('FillsTable', () => {
|
||||
@@ -75,7 +75,7 @@ describe('FillsTable', () => {
|
||||
'1.00 BTC',
|
||||
'3.00 BTC',
|
||||
'Maker',
|
||||
'0.06 BTC',
|
||||
'2.00 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
@@ -176,4 +176,36 @@ describe('FillsTable', () => {
|
||||
await screen.findByTestId('fee-breakdown-tooltip')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
};
|
||||
expect(getFeesBreakdown('TAKER', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '-1000',
|
||||
totalFee: '4000',
|
||||
};
|
||||
expect(getFeesBreakdown('MAKER', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user