Feat/348 select size of trade (#943)

* feat(ui-toolkit): add slider to ui-toolkit

* feat(console-lite): add deal ticket size to use new slider

* feat(console-lite): add use-maximum-position-size hook

* feat(console-lite): add e2e tests

* feat(console-lite): add position size value after selection

* fix(console-lite): remove lingering console log

* fix(console-lite): fix linting errors

* fix(console-lite): fix cypress config with wrong app specified

* fix(console-lite): fix react hooks bug after upgrade to react 18

* feat(console-lite): add proportional size selector and size amount input

* fix(console-lite): add missing env variables

* feat(console-lite): add missing tests and fix broken one after input button for size

* fix(console-lite): fix async error for max trade size
This commit is contained in:
Elmar 2022-08-10 16:28:03 +01:00 committed by GitHub
parent 1a1ab4db65
commit b75ed62072
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 853 additions and 45 deletions

View File

@ -12,8 +12,9 @@ module.exports = defineConfig({
supportFile: './src/support/index.ts',
video: true,
videoUploadOnPasses: false,
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
videosFolder: '../../dist/cypress/apps/simple-trading-app-e2e/videos',
screenshotsFolder:
'../../dist/cypress/apps/simple-trading-app-e2e/screenshots',
chromeWebSecurity: false,
viewportWidth: 1440,
viewportHeight: 900,

View File

@ -29,6 +29,7 @@ describe('Market trade', () => {
}
});
});
it('side selector should work well', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[0].id}`);
@ -46,7 +47,7 @@ describe('Market trade', () => {
}
});
it('mobile view should work well', () => {
it('side selector mobile view should work well', () => {
if (markets?.length) {
cy.viewport('iphone-xr');
cy.visit(`/trading/${markets[0].id}`);
@ -78,6 +79,79 @@ describe('Market trade', () => {
}
});
it('size slider should work well', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[1].id}`);
connectVegaWallet();
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.get('#step-2-panel').find('[role="slider"]').type('{rightarrow}');
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '2');
}
});
it('percentage selection should work well', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[1].id}`);
connectVegaWallet();
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.getByTestId('percentage-selector')
.find('button')
.contains('Max')
.click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '21');
}
});
it('size input should work well', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[1].id}`);
connectVegaWallet();
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('input')
.type('{backspace}2');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '2');
}
});
it('order review should display proper calculations', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[0].id}`);

View File

@ -4,6 +4,7 @@ export const generateMarketPositions = () => {
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
accounts: [
{
type: 'General',
asset: {
decimals: 5,
},
@ -14,6 +15,7 @@ export const generateMarketPositions = () => {
},
},
{
type: 'Margin',
asset: {
decimals: 5,
},

View File

@ -4,6 +4,7 @@ export const generatePartyBalance = () => {
accounts: [
{
balance: '88474051',
type: 'General',
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
@ -15,6 +16,7 @@ export const generatePartyBalance = () => {
},
{
balance: '100000000',
type: 'General',
asset: {
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
symbol: 'tEURO',
@ -26,6 +28,7 @@ export const generatePartyBalance = () => {
},
{
balance: '3412867',
type: 'General',
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
@ -37,6 +40,7 @@ export const generatePartyBalance = () => {
},
{
balance: '70007',
type: 'General',
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',

View File

@ -17,8 +17,11 @@ NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
NX_VEGA_CONFIG_URL="https://static.vega.xyz/assets/testnet-network.json"
NX_VEGA_ENV = 'TESTNET'
NX_VEGA_URL="https://lb.testnet.vega.xyz/query"
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf

View File

@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
NX_VEGA_URL=https://n04.d.vega.xyz/query
NX_VEGA_ENV=DEVNET
NX_VEGA_REST=https://n04.d.vega.xyz/datanode/rest
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz

View File

@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
NX_VEGA_URL=https://api.token.vega.xyz/query
NX_VEGA_ENV=MAINNET
NX_VEGA_REST=https://api.token.vega.xyz/
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz

View File

@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_URL=https://n03.s.vega.xyz/query
NX_VEGA_ENV=STAGNET
NX_VEGA_REST=https://n03.s.vega.xyz/datanode/rest
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_EXPLORER_URL=https://staging.explorer.vega.xyz

View File

@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet2-network.json
NX_VEGA_URL=https://n03.stagnet2.vega.xyz/query
NX_VEGA_ENV=STAGNET2
NX_VEGA_REST=https://n01.stagnet2.vega.xyz/datanode/rest
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_EXPLORER_URL=https://staging2.explorer.vega.xyz

View File

@ -3,3 +3,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
NX_VEGA_ENV=TESTNET
NX_VEGA_REST=https://lb.testnet.vega.xyz/datanode/rest
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf

View File

@ -3,6 +3,8 @@
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: PartyBalanceQuery
// ====================================================
@ -29,6 +31,10 @@ export interface PartyBalanceQuery_party_accounts_asset {
export interface PartyBalanceQuery_party_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/

View File

@ -5,6 +5,7 @@ import type {
PartyBalanceQuery_party_accounts_asset,
} from './__generated__/PartyBalanceQuery';
import { DealTicketBalance } from './deal-ticket-balance';
import { AccountType } from '@vegaprotocol/types';
const tDAI: PartyBalanceQuery_party_accounts_asset = {
__typename: 'Asset',
@ -17,6 +18,7 @@ const tDAI: PartyBalanceQuery_party_accounts_asset = {
const accounts: PartyBalanceQuery_party_accounts[] = [
{
__typename: 'Account',
type: AccountType.General,
balance: '1000000',
asset: tDAI,
},

View File

@ -4,6 +4,7 @@ import type { DealTicketQuery_market_tradableInstrument_instrument_product_settl
import type { PartyBalanceQuery_party_accounts } from './__generated__/PartyBalanceQuery';
import { useSettlementAccount } from '../../hooks/use-settlement-account';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import { AccountType } from '@vegaprotocol/types';
interface DealTicketBalanceProps {
settlementAsset: DealTicketQuery_market_tradableInstrument_instrument_product_settlementAsset;
@ -20,7 +21,11 @@ export const DealTicketBalance = ({
}: DealTicketBalanceProps) => {
const settlementAssetId = settlementAsset?.id;
const settlementAssetSymbol = settlementAsset?.symbol;
const settlementAccount = useSettlementAccount(settlementAssetId, accounts);
const settlementAccount = useSettlementAccount(
settlementAssetId,
accounts,
AccountType.General
);
const formatedNumber =
settlementAccount?.balance &&
settlementAccount.asset.decimals &&

View File

@ -17,6 +17,7 @@ const PARTY_BALANCE_QUERY = gql`
query PartyBalanceQuery($partyId: ID!) {
party(id: $partyId) {
accounts {
type
balance
asset {
id

View File

@ -0,0 +1,201 @@
import React, { useCallback, useState } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import {
SliderRoot,
SliderThumb,
SliderTrack,
SliderRange,
Button,
Input,
FormGroup,
} from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js';
interface DealTicketSizeProps {
step: number;
min: number;
max: number;
value: number;
onValueChange: (value: number[]) => void;
name: string;
quoteName: string;
price: string;
estCloseOut: string;
estMargin: string;
positionDecimalPlaces: number;
}
const getSizeLabel = (value: number): string => {
const MIN_LABEL = 'Min';
const MAX_LABEL = 'Max';
if (value === 0) {
return MIN_LABEL;
} else if (value === 100) {
return MAX_LABEL;
}
return `${value}%`;
};
export const DealTicketSize = ({
value,
step,
min,
max,
price,
quoteName,
onValueChange,
estCloseOut,
positionDecimalPlaces,
}: DealTicketSizeProps) => {
const sizeRatios = [0, 25, 50, 75, 100];
const [inputValue, setInputValue] = useState(value);
const [isInputVisible, setIsInputVisible] = useState(false);
const onInputValueChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(event.target.value);
const isLessThanMin = value < min;
const isMoreThanMax = value > max;
if (value) {
if (isLessThanMin) {
onValueChange([min]);
} else if (isMoreThanMax) {
onValueChange([max]);
} else {
onValueChange([value]);
}
}
setInputValue(value);
},
[min, max, onValueChange, setInputValue]
);
const onButtonValueChange = useCallback(
(size: number) => {
if (isInputVisible) {
setIsInputVisible(false);
}
const newVal = new BigNumber(size)
.decimalPlaces(positionDecimalPlaces)
.toNumber();
onValueChange([newVal]);
setInputValue(newVal);
},
[isInputVisible, onValueChange, positionDecimalPlaces]
);
const toggleInput = useCallback(() => {
setIsInputVisible(!isInputVisible);
}, [isInputVisible]);
const onInputEnter = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
event.stopPropagation();
toggleInput();
}
},
[toggleInput]
);
return max === 0 ? (
<p>Not enough balance to trade</p>
) : (
<div>
<div className="flex justify-between text-black dark:text-white mb-8">
<span>{min}</span>
<span>{max}</span>
</div>
<SliderRoot
className="mb-8"
value={[value]}
onValueChange={onValueChange}
step={step}
min={min}
max={max}
>
<SliderTrack className="bg-lightGrey dark:bg-offBlack">
<SliderRange className="!bg-black dark:!bg-white" />
</SliderTrack>
<SliderThumb />
</SliderRoot>
<div
data-testid="percentage-selector"
className="flex w-full justify-between text-black dark:text-white mb-32"
>
{sizeRatios.map((size, index) => {
const proportionalSize = size ? (size / 100) * max : min;
return (
<Button
variant="inline-link"
className="no-underline !text-blue"
onClick={() => onButtonValueChange(proportionalSize)}
key={index}
>
{getSizeLabel(size)}
</Button>
);
})}
</div>
<dl className="text-black dark:text-white">
<div className="flex items-center justify-between mb-8">
<dt>
<span>{t('Size')}</span>
&nbsp;
<small>({quoteName})</small>
</dt>
<dd className="flex justify-end w-full">
<FormGroup
className="mb-0 flex items-center"
labelClassName="mr-8 sr-only"
label="Enter Size"
labelFor="trade-size-input"
>
{isInputVisible ? (
<>
<Input
id="input-order-size-market"
type="number"
step={step}
min={min}
max={max}
className="w-full"
value={inputValue}
onKeyDown={onInputEnter}
onChange={onInputValueChange}
/>
<Button
variant="inline-link"
className="no-underline !text-blue"
onClick={toggleInput}
>
{t('set')}
</Button>
</>
) : (
<Button
variant="inline-link"
className="no-underline !text-blue"
onClick={toggleInput}
>
{value}
</Button>
)}
</FormGroup>
</dd>
</div>
<div className="flex justify-between mb-8">
<dt>{t('Est. price')}</dt>
<dd>{price}</dd>
</div>
<div className="flex justify-between">
<dt>{t('Est. close out')}</dt>
<dd>{estCloseOut}</dd>
</div>
</dl>
</div>
);
};

View File

@ -1,29 +1,34 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { InputError } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js';
import {
DealTicketAmount,
getOrderDialogTitle,
getOrderDialogIntent,
getOrderDialogIcon,
MarketSelector,
} from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
import {
getDefaultOrder,
useOrderValidation,
useOrderSubmit,
OrderFeedback,
validateSize,
} from '@vegaprotocol/orders';
import { DealTicketSize } from './deal-ticket-size';
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';
import SideSelector, { SIDE_NAMES } from './side-selector';
import ReviewTrade from './review-trade';
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
import useOrderCloseOut from '../../hooks/use-order-closeout';
import useOrderMargin from '../../hooks/use-order-margin';
import useMaximumPositionSize from '../../hooks/use-maximum-position-size';
interface DealTicketMarketProps {
market: DealTicketQuery_market;
@ -43,21 +48,48 @@ export const DealTicketSteps = ({
);
const {
register,
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<Order>({
mode: 'onChange',
defaultValues: getDefaultOrder(market),
});
const [max, setMax] = useState<number | null>(null);
const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const orderSide = watch('side');
const orderSize = watch('size');
const order = watch();
const estCloseOut = useOrderCloseOut({ order, market, partyData });
const { keypair } = useVegaWallet();
const estMargin = useOrderMargin({
order,
market,
partyId: keypair?.pub || '',
});
const maxTrade = useMaximumPositionSize({
partyId: keypair?.pub || '',
accounts: partyData?.party?.accounts || [],
marketId: market.id,
settlementAssetId:
market.tradableInstrument.instrument.product.settlementAsset.id,
price: market?.depth?.lastTrade?.price,
order,
});
useEffect(() => {
setMax(
new BigNumber(maxTrade)
.decimalPlaces(market.positionDecimalPlaces)
.toNumber()
);
}, [maxTrade, market.positionDecimalPlaces]);
const { message: invalidText, isDisabled } = useOrderValidation({
step,
@ -70,6 +102,16 @@ export const DealTicketSteps = ({
const { submit, transaction, finalizedOrder, TransactionDialog } =
useOrderSubmit(market);
const onSizeChange = (value: number[]) => {
const newVal = new BigNumber(value[0])
.decimalPlaces(market.positionDecimalPlaces)
.toString();
const isValid = validateSize(step)(newVal);
if (isValid !== 'step') {
setValue('size', newVal);
}
};
const transactionStatus =
transaction.status === VegaTxStatus.Requested ||
transaction.status === VegaTxStatus.Pending
@ -112,19 +154,29 @@ export const DealTicketSteps = ({
},
{
label: t('Choose Position Size'),
component: (
<DealTicketAmount
orderType={orderType}
step={step}
register={register}
price={
market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: undefined
}
quoteName={market.tradableInstrument.instrument.product.quoteName}
/>
),
component:
max !== null ? (
<DealTicketSize
step={step}
min={step}
max={max}
onValueChange={onSizeChange}
value={new BigNumber(orderSize).toNumber()}
name="size"
price={
market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: ''
}
positionDecimalPlaces={market.positionDecimalPlaces}
quoteName={market.tradableInstrument.instrument.product.quoteName}
estCloseOut={estCloseOut}
estMargin={estMargin || ' - '}
/>
) : (
'loading...'
),
value: orderSize,
},
{
label: t('Review Trade'),
@ -140,7 +192,8 @@ export const DealTicketSteps = ({
isDisabled={isDisabled}
transactionStatus={transactionStatus}
order={order}
partyData={partyData}
estCloseOut={estCloseOut}
estMargin={estMargin || ' - '}
/>
<TransactionDialog
title={getOrderDialogTitle(finalizedOrder?.status)}

View File

@ -1 +1,2 @@
export { DealTicketContainer } from './deal-ticket-container';
export * from './deal-ticket-container';
export * from './deal-ticket-size';

View File

@ -10,17 +10,14 @@ import classNames from 'classnames';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders';
import { SIDE_NAMES } from './side-selector';
import { useVegaWallet, VegaWalletOrderSide } from '@vegaprotocol/wallet';
import { VegaWalletOrderSide } from '@vegaprotocol/wallet';
import SimpleMarketExpires from '../simple-market-list/simple-market-expires';
import { gql, useQuery } from '@apollo/client';
import type {
MarketTags,
MarketTagsVariables,
} from './__generated__/MarketTags';
import useOrderMargin from '../../hooks/use-order-margin';
import useOrderCloseOut from '../../hooks/use-order-closeout';
import { IconNames } from '@blueprintjs/icons';
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
export const MARKET_TAGS_QUERY = gql`
query MarketTags($marketId: ID!) {
@ -41,7 +38,8 @@ interface Props {
isDisabled: boolean;
transactionStatus?: string;
order: Order;
partyData?: PartyBalanceQuery;
estCloseOut: string;
estMargin: string;
}
export default ({
@ -49,21 +47,16 @@ export default ({
market,
order,
transactionStatus,
partyData,
estCloseOut,
estMargin,
}: Props) => {
const { keypair } = useVegaWallet();
const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>(
MARKET_TAGS_QUERY,
{
variables: { marketId: market.id },
}
);
const estMargin = useOrderMargin({
order,
market,
partyId: keypair?.pub || '',
});
const estCloseOut = useOrderCloseOut({ order, market, partyData });
return (
<div className="mb-8 text-black dark:text-white">
<KeyValueTable>

View File

@ -0,0 +1,117 @@
import { renderHook } from '@testing-library/react';
import useMaximumPositionSize from './use-maximum-position-size';
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { AccountType } from '@vegaprotocol/types';
import type { PositionMargin } from './use-market-positions';
import { BigNumber } from 'bignumber.js';
import {
VegaWalletOrderSide,
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
const defaultMockMarketPositions = {
openVolume: new BigNumber(1),
balance: new BigNumber(100000),
};
let mockMarketPositions: PositionMargin | null = defaultMockMarketPositions;
const mockAccount: PartyBalanceQuery_party_accounts = {
__typename: 'Account',
type: AccountType.General,
balance: '200000',
asset: {
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
name: 'tBTC TEST',
decimals: 5,
},
};
const mockOrder = {
type: VegaWalletOrderType.Market,
size: '1',
side: VegaWalletOrderSide.Buy,
timeInForce: VegaWalletOrderTimeInForce.IOC,
};
jest.mock('./use-settlement-account', () => {
return {
useSettlementAccount: jest.fn(() => mockAccount),
};
});
jest.mock('./use-market-positions', () => jest.fn(() => mockMarketPositions));
describe('useMaximumPositionSize Hook', () => {
it('should return correct size when no open positions', () => {
mockMarketPositions = null;
const price = '50';
const expected = 4000;
const { result } = renderHook(() =>
useMaximumPositionSize({
marketId: '',
partyId: '',
price,
settlementAssetId: '',
order: mockOrder,
accounts: [mockAccount],
})
);
expect(result.current).toBe(expected);
});
it('should return correct size when open positions and same side', () => {
const price = '50';
mockMarketPositions = defaultMockMarketPositions;
const expected = 3999;
const { result } = renderHook(() =>
useMaximumPositionSize({
marketId: '',
partyId: '',
price,
settlementAssetId: '',
order: mockOrder,
accounts: [mockAccount],
})
);
expect(result.current).toBe(expected);
});
it('should return correct size when open positions and opposite side', () => {
const price = '50';
mockOrder.side = VegaWalletOrderSide.Sell;
mockMarketPositions = defaultMockMarketPositions;
const expected = 4001;
const { result } = renderHook(() =>
useMaximumPositionSize({
marketId: '',
partyId: '',
price,
settlementAssetId: '',
order: mockOrder,
accounts: [mockAccount],
})
);
expect(result.current).toBe(expected);
});
it('should return zero if no account balance', () => {
mockAccount.balance = '0';
const price = '50';
mockMarketPositions = defaultMockMarketPositions;
const expected = 0;
const { result } = renderHook(() =>
useMaximumPositionSize({
marketId: '',
partyId: '',
price,
settlementAssetId: '',
order: mockOrder,
accounts: [],
})
);
expect(result.current).toBe(expected);
});
});

View File

@ -0,0 +1,61 @@
import useMarketPositions from './use-market-positions';
import type { Order } from '@vegaprotocol/orders';
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { useSettlementAccount } from './use-settlement-account';
import { AccountType } from '@vegaprotocol/types';
import { VegaWalletOrderSide } from '@vegaprotocol/wallet';
import { BigNumber } from 'bignumber.js';
interface Props {
partyId: string;
accounts: PartyBalanceQuery_party_accounts[];
marketId: string;
price?: string;
settlementAssetId: string;
order: Order;
}
const getSize = (balance: string, price: string) =>
new BigNumber(balance).dividedBy(new BigNumber(price));
export default ({
marketId,
accounts,
partyId,
price,
settlementAssetId,
order,
}: Props): number => {
const settlementAccount = useSettlementAccount(
settlementAssetId,
accounts,
AccountType.General
);
const marketPositions = useMarketPositions({ marketId: marketId, partyId });
if (
!settlementAccount?.balance ||
new BigNumber(settlementAccount?.balance || 0).isZero()
) {
return 0;
}
const size = getSize(settlementAccount.balance, price || '');
if (!marketPositions) {
return size.toNumber() || 0;
}
const isSameSide =
(marketPositions.openVolume.isPositive() &&
order.side === VegaWalletOrderSide.Buy) ||
(marketPositions.openVolume.isNegative() &&
order.side === VegaWalletOrderSide.Sell);
const adjustedForVolume = new BigNumber(size)[isSameSide ? 'minus' : 'plus'](
marketPositions.openVolume
);
return adjustedForVolume.isNegative() ? 0 : adjustedForVolume.toNumber();
};

View File

@ -1,12 +1,14 @@
import { renderHook } from '@testing-library/react';
import { useSettlementAccount } from './use-settlement-account';
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { AccountType } from '@vegaprotocol/types';
describe('useSettlementAccount Hook', () => {
it('should filter accounts by settlementAssetId', () => {
const accounts: PartyBalanceQuery_party_accounts[] = [
{
__typename: 'Account',
type: AccountType.General,
balance: '2000000000000000000000',
asset: {
__typename: 'Asset',
@ -18,6 +20,7 @@ describe('useSettlementAccount Hook', () => {
},
{
__typename: 'Account',
type: AccountType.General,
balance: '1000000000',
asset: {
__typename: 'Asset',
@ -29,6 +32,19 @@ describe('useSettlementAccount Hook', () => {
},
{
__typename: 'Account',
type: AccountType.General,
balance: '5000000000000000000',
asset: {
__typename: 'Asset',
id: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55',
symbol: 'VEGA',
name: 'Vega (testnet)',
decimals: 18,
},
},
{
__typename: 'Account',
type: AccountType.Margin,
balance: '5000000000000000000',
asset: {
__typename: 'Asset',
@ -39,14 +55,23 @@ describe('useSettlementAccount Hook', () => {
},
},
];
const settlementAssetId =
const tDAI =
'6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61';
const vega =
'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55';
const { result } = renderHook(() =>
useSettlementAccount(settlementAssetId, accounts)
const { result: resultDai } = renderHook(() =>
useSettlementAccount(tDAI, accounts)
);
expect(result.current?.balance).toBe(accounts[1].balance);
expect(result.current?.asset).toEqual(accounts[1].asset);
expect(resultDai.current?.balance).toBe(accounts[1].balance);
expect(resultDai.current?.asset).toEqual(accounts[1].asset);
const { result: resultVega } = renderHook(() =>
useSettlementAccount(vega, accounts, AccountType.Margin)
);
expect(resultVega.current?.balance).toBe(accounts[3].balance);
expect(resultVega.current?.asset).toEqual(accounts[3].asset);
});
it('should return null if no accounts', () => {

View File

@ -1,12 +1,20 @@
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import type { AccountType } from '@vegaprotocol/types';
import { useMemo } from 'react';
export const useSettlementAccount = (
settlementAssetId: string,
accounts: PartyBalanceQuery_party_accounts[]
accounts: PartyBalanceQuery_party_accounts[],
type?: AccountType
): PartyBalanceQuery_party_accounts | null => {
const callback = () =>
accounts.find((account) => account.asset.id === settlementAssetId);
const account = useMemo(callback, [accounts, settlementAssetId]);
accounts.find((account) => {
if (type) {
return account.asset.id === settlementAssetId && account.type === type;
}
return account.asset.id === settlementAssetId;
});
const account = useMemo(callback, [accounts, settlementAssetId, type]);
return account as PartyBalanceQuery_party_accounts;
};

View File

@ -31,3 +31,4 @@ export * from './theme-switcher';
export * from './toggle';
export * from './tooltip';
export * from './vega-logo';
export * from './slider';

View File

@ -0,0 +1 @@
export * from './slider';

View File

@ -0,0 +1,27 @@
import type { Story, Meta } from '@storybook/react';
import { Slider } from './slider';
import { useState } from 'react';
export default {
component: Slider,
title: 'Slider',
} as Meta;
const Template: Story = ({ value: val, ...args }) => {
const [value, setValue] = useState(val);
const onValueChange = (val: [number]) => {
setValue(val);
};
return <Slider onValueChange={onValueChange} value={value} {...args} />;
};
export const Default = Template.bind({});
Default.args = {
min: 0,
max: 1000,
step: 100,
value: [100],
};

View File

@ -0,0 +1,84 @@
import React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import type {
SliderProps,
SliderTrackProps,
SliderRangeProps,
SliderThumbProps,
} from '@radix-ui/react-slider';
import classNames from 'classnames';
export const SliderRoot = ({
children,
className,
orientation = 'horizontal',
...props
}: SliderProps) => {
const defaultStyles = 'relative flex items-center select-none touch-none';
const classes = classNames(
defaultStyles,
{
'h-[20px] w-full': orientation === 'horizontal',
'flex-col w-[20px] h-full': orientation === 'vertical',
},
className
);
return (
<SliderPrimitive.Root
orientation={orientation}
className={classes}
{...props}
>
{children}
</SliderPrimitive.Root>
);
};
export const SliderTrack = ({
children,
className,
...props
}: SliderTrackProps) => {
const defaultStyles = 'bg-black dark:bg-white relative grow h-[3px]';
return (
<SliderPrimitive.Track
className={classNames(defaultStyles, className)}
{...props}
>
{children}
</SliderPrimitive.Track>
);
};
export const SliderRange = ({ className, ...props }: SliderRangeProps) => {
const defaultStyles = 'absolute bg-blue h-full';
return (
<SliderPrimitive.Range
className={classNames(defaultStyles, className)}
{...props}
/>
);
};
export const SliderThumb = ({ className, ...props }: SliderThumbProps) => {
const defaultStyles =
'block w-[20px] h-[20px] border-2 border-black dark:border-white bg-white dark:bg-black rounded-full';
return (
<SliderPrimitive.Thumb
className={classNames(defaultStyles, className)}
{...props}
/>
);
};
export const Slider = (props: SliderProps) => {
return (
<SliderRoot {...props}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</SliderRoot>
);
};

View File

@ -27,6 +27,7 @@
"@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-radio-group": "^0.1.5",
"@radix-ui/react-select": "^0.1.1",
"@radix-ui/react-slider": "^1.0.0",
"@radix-ui/react-tabs": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7",
"@sentry/nextjs": "^6.19.3",

117
yarn.lock
View File

@ -3281,6 +3281,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/number@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.0.0.tgz#4c536161d0de750b3f5d55860fc3de46264f897b"
integrity sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/popper@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/popper/-/popper-0.1.0.tgz#c387a38f31b7799e1ea0d2bb1ca0c91c2931b063"
@ -3296,6 +3303,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253"
integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-accordion@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-0.1.6.tgz#b76613d56717ed24b8cf6cb1897cbd54f04714ed"
@ -3345,6 +3359,17 @@
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-slot" "0.1.2"
"@radix-ui/react-collection@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.0.tgz#0ec4c72fabd35a03b5787075ac799e3b17ca5710"
integrity sha512-8i1pf5dKjnq90Z8udnnXKzdCEV3/FYrfw0n/b6NvB6piXEn3fO1bOh7HBcpG8XrnIXzxlYu2oCcR38QpyLS/mg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-slot" "1.0.0"
"@radix-ui/react-compose-refs@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz#cff6e780a0f73778b976acff2c2a5b6551caab95"
@ -3352,6 +3377,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-0.1.1.tgz#06996829ea124d9a1bc1dbe3e51f33588fab0875"
@ -3359,6 +3391,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dialog@^0.1.5":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.1.7.tgz#285414cf66f5bbf42bc9935314e0381abe01e7d0"
@ -3380,6 +3419,13 @@
aria-hidden "^1.1.1"
react-remove-scroll "^2.4.0"
"@radix-ui/react-direction@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45"
integrity sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dismissable-layer@0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.1.5.tgz#9379032351e79028d472733a5cc8ba4a0ea43314"
@ -3513,6 +3559,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "0.1.2"
"@radix-ui/react-primitive@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz#376cd72b0fcd5e0e04d252ed33eb1b1f025af2b0"
integrity sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.0"
"@radix-ui/react-radio-group@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.1.5.tgz#ca8a676123a18b44804aff10af46129e2c2b37c3"
@ -3570,6 +3624,24 @@
aria-hidden "^1.1.1"
react-remove-scroll "^2.4.0"
"@radix-ui/react-slider@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.0.0.tgz#4cabadd243aa088eb45ac710cd7cdc518fafb07e"
integrity sha512-LMZET7vn7HYwYSjsc9Jcen8Vn4cJXZZxQT7T+lGlqp+F+FofX+H86TBF2yDq+L51d99f1KLEsflTGBz9WRLSig==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/number" "1.0.0"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-collection" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-direction" "1.0.0"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-use-previous" "1.0.0"
"@radix-ui/react-use-size" "1.0.0"
"@radix-ui/react-slot@0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-0.1.2.tgz#e6f7ad9caa8ce81cc8d532c854c56f9b8b6307c8"
@ -3578,6 +3650,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "0.1.0"
"@radix-ui/react-slot@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698"
integrity sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-tabs@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-0.1.5.tgz#ddcf860cc32e186d76477ae767dbb216d1944252"
@ -3627,6 +3707,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90"
integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-controllable-state@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz#4fced164acfc69a4e34fb9d193afdab973a55de1"
@ -3635,6 +3722,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-use-controllable-state@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-direction@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-direction/-/react-use-direction-0.1.0.tgz#97ac1d52e497c974389e7988f809238ed72e7df7"
@ -3657,6 +3752,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc"
integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz#0226017f72267200f6e832a7103760e96a6db5d0"
@ -3664,6 +3766,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.0.tgz#e48a69c3a7d8078a967084038df66d0d181c56ac"
integrity sha512-RG2K8z/K7InnOKpq6YLDmT49HGjNmrK+fr82UCVKT2sW0GYfVnYp4wZWBooT/EYfQ5faA9uIjvsuMMhH61rheg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-rect@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.1.1.tgz#6c15384beee59c086e75b89a7e66f3d2e583a856"
@ -3679,6 +3788,14 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-size@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz#a0b455ac826749419f6354dc733e2ca465054771"
integrity sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-visually-hidden@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz#6c75eae34fb5d084b503506fbfc05587ced05f03"