feat(trading): show positions for all connected keys (#3858)

This commit is contained in:
Bartłomiej Głownia 2023-05-25 12:59:08 +02:00 committed by GitHub
parent 1ae1fdff5e
commit f364dabe2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 424 additions and 419 deletions

View File

@ -1,6 +1,7 @@
import { checkSorting } from '@vegaprotocol/cypress'; import { checkSorting } from '@vegaprotocol/cypress';
import { aliasGQLQuery } from '@vegaprotocol/cypress'; import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock'; import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock';
beforeEach(() => { beforeEach(() => {
cy.mockTradingPage(); cy.mockTradingPage();
@ -16,9 +17,27 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
}); });
it('renders positions on portfolio page', () => { it('renders positions on portfolio page', () => {
cy.mockGQL((req) => {
const positions = positionsQuery();
if (positions.positions?.edges) {
positions.positions.edges.push(
...positions.positions.edges.map((edge) => ({
...edge,
node: {
...edge.node,
party: {
...edge.node.party,
id: 'vega-1',
},
},
}))
);
}
aliasGQLQuery(req, 'Positions', positions);
});
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.getByTestId('Positions').click(); cy.getByTestId('Positions').click();
validatePositionsDisplayed(); validatePositionsDisplayed(true);
}); });
describe('renders position among some graphql errors', () => { describe('renders position among some graphql errors', () => {
it('rows should be displayed despite errors', () => { it('rows should be displayed despite errors', () => {
@ -56,7 +75,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('tab-positions') cy.getByTestId('tab-positions')
.first() .first()
.within(() => { .within(() => {
cy.get('[row-id="market-2"]') cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(1) .eq(1)
.within(() => { .within(() => {
emptyCells.forEach((cell) => { emptyCells.forEach((cell) => {
@ -103,9 +124,11 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
const marketsSortedDefault = [ const marketsSortedDefault = [
'ACTIVE MARKET', 'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)', 'Apple Monthly (30 Jun 2022)',
'SUSPENDED MARKET',
]; ];
const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)']; const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)'];
const marketsSortedDesc = [ const marketsSortedDesc = [
'SUSPENDED MARKET',
'Apple Monthly (30 Jun 2022)', 'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET', 'ACTIVE MARKET',
]; ];
@ -119,9 +142,13 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
}); });
it('sorting by notional', () => { it('sorting by notional', () => {
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['276,761.40348', '46,126.90058']; const marketsSortedDefault = [
const marketsSortedAsc = ['46,126.90058', '276,761.40348']; '276,761.40348',
const marketsSortedDesc = ['276,761.40348', '46,126.90058']; '46,126.90058',
'1,688.20',
];
const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20'];
cy.getByTestId('Positions').click(); cy.getByTestId('Positions').click();
checkSorting( checkSorting(
'notional', 'notional',
@ -132,9 +159,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
}); });
it('sorting by unrealisedPNL', () => { it('sorting by unrealisedPNL', () => {
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['8.95', '-0.22519']; const marketsSortedDefault = ['8.95', '-0.22519', '8.95'];
const marketsSortedAsc = ['-0.22519', '8.95']; const marketsSortedAsc = ['-0.22519', '8.95', '8.95'];
const marketsSortedDesc = ['8.95', '-0.22519']; const marketsSortedDesc = ['8.95', '8.95', '-0.22519'];
cy.getByTestId('Positions').click(); cy.getByTestId('Positions').click();
checkSorting( checkSorting(
'unrealisedPNL', 'unrealisedPNL',
@ -145,7 +172,7 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
}); });
}); });
function validatePositionsDisplayed() { function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible'); cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => { cy.getByTestId('tab-positions').within(() => {
cy.get('[col-id="marketName"]') cy.get('[col-id="marketName"]')
@ -165,10 +192,11 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.wrap($prices).invoke('text').should('not.be.empty'); cy.wrap($prices).invoke('text').should('not.be.empty');
}); });
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1'); if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="marginAccountBalance"]') // margin allocated cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01'); .should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => { cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty'); cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
@ -184,6 +212,6 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume'); cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
}); });
cy.getByTestId('close-position').should('be.visible').and('have.length', 2); cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
} }
}); });

View File

@ -175,6 +175,10 @@ describe('Closed', () => {
__typename: 'Market', __typename: 'Market',
id: marketId, id: marketId,
}, },
party: {
__typename: 'Party',
id: pubKey,
},
}; };
}; };
const position = createPosition(); const position = createPosition();
@ -182,18 +186,14 @@ describe('Closed', () => {
request: { request: {
query: PositionsDocument, query: PositionsDocument,
variables: { variables: {
partyId: pubKey, partyIds: [pubKey],
}, },
}, },
result: { result: {
data: { data: {
party: { positions: {
__typename: 'Party', __typename: 'PositionConnection',
id: pubKey, edges: [{ __typename: 'PositionEdge', node: position }],
positionsConnection: {
__typename: 'PositionConnection',
edges: [{ __typename: 'PositionEdge', node: position }],
},
}, },
}, },
}, },

View File

@ -64,7 +64,7 @@ export const Closed = () => {
}); });
const { data: positionData } = usePositionsQuery({ const { data: positionData } = usePositionsQuery({
variables: { variables: {
partyId: pubKey || '', partyIds: pubKey ? [pubKey] : [],
}, },
skip: !pubKey, skip: !pubKey,
}); });
@ -72,11 +72,9 @@ export const Closed = () => {
// find a position for each market and add the realised pnl to // find a position for each market and add the realised pnl to
// a normalized object // a normalized object
const rowData = compact(marketData).map((market) => { const rowData = compact(marketData).map((market) => {
const position = positionData?.party?.positionsConnection?.edges?.find( const position = positionData?.positions?.edges?.find((edge) => {
(edge) => { return edge.node.market.id === market.id;
return edge.node.market.id === market.id; });
}
);
const instrument = market.tradableInstrument.instrument; const instrument = market.tradableInstrument.instrument;

View File

@ -54,6 +54,7 @@ export const Portfolio = () => {
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
noBottomPlaceholder noBottomPlaceholder
storeKey="portfolioPositions" storeKey="portfolioPositions"
allKeys
/> />
</VegaWalletContainer> </VegaWalletContainer>
</Tab> </Tab>

View File

@ -89,6 +89,9 @@ const cacheConfig: InMemoryCacheConfig = {
Party: { Party: {
keyFields: false, keyFields: false,
}, },
Position: {
keyFields: ['market', ['id'], 'party', ['id']],
},
Fees: { Fees: {
keyFields: false, keyFields: false,
}, },

View File

@ -1,10 +1,9 @@
import { useRef, useMemo, memo, useCallback } from 'react'; import { useRef, useMemo, memo } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import { aggregatedAccountsDataProvider } from './accounts-data-provider'; import { aggregatedAccountsDataProvider } from './accounts-data-provider';
import type { PinnedAsset } from './accounts-table'; import type { PinnedAsset } from './accounts-table';
import { AccountTable } from './accounts-table'; import { AccountTable } from './accounts-table';
@ -36,16 +35,8 @@ export const AccountManager = ({
dataProvider: aggregatedAccountsDataProvider, dataProvider: aggregatedAccountsDataProvider,
variables, variables,
}); });
const setId = useCallback( const bottomPlaceholderProps = useBottomPlaceholder({
(data: AccountFields, id: string) => ({
...data,
asset: { ...data.asset, id },
}),
[]
);
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
gridRef, gridRef,
setId,
disabled: noBottomPlaceholder, disabled: noBottomPlaceholder,
}); });

View File

@ -154,7 +154,11 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
{...props} {...props}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No accounts')} overlayNoRowsTemplate={t('No accounts')}
getRowId={({ data }: { data: AccountFields }) => data.asset.id} getRowId={({
data,
}: {
data: AccountFields & { isLastPlaceholder?: boolean; id?: string };
}) => (data.isLastPlaceholder && data.id ? data.id : data.asset.id)}
ref={ref} ref={ref}
tooltipShowDelay={500} tooltipShowDelay={500}
rowData={props.rowData?.filter( rowData={props.rowData?.filter(

View File

@ -164,7 +164,9 @@ interface DataProviderParams<
Data, Data,
SubscriptionData, SubscriptionData,
Delta, Delta,
Variables extends OperationVariables | undefined = undefined Variables extends OperationVariables | undefined = undefined,
SubscriptionVariables extends OperationVariables | undefined = Variables,
QueryVariables extends OperationVariables | undefined = Variables
> { > {
query: Query<QueryData>; query: Query<QueryData>;
subscriptionQuery?: Query<SubscriptionData>; subscriptionQuery?: Query<SubscriptionData>;
@ -181,6 +183,10 @@ interface DataProviderParams<
resetDelay?: number; resetDelay?: number;
additionalContext?: Record<string, unknown>; additionalContext?: Record<string, unknown>;
errorPolicyGuard?: (graphqlErrors: GraphQLErrors) => boolean; errorPolicyGuard?: (graphqlErrors: GraphQLErrors) => boolean;
getQueryVariables?: (variables: Variables) => QueryVariables;
getSubscriptionVariables?: (
variables: Variables
) => SubscriptionVariables | SubscriptionVariables[];
} }
/** /**
@ -199,7 +205,9 @@ function makeDataProviderInternal<
Data, Data,
SubscriptionData, SubscriptionData,
Delta, Delta,
Variables extends OperationVariables | undefined = undefined Variables extends OperationVariables | undefined = undefined,
QueryVariables extends OperationVariables | undefined = Variables,
SubscriptionVariables extends OperationVariables | undefined = Variables
>({ >({
query, query,
subscriptionQuery, subscriptionQuery,
@ -211,12 +219,16 @@ function makeDataProviderInternal<
resetDelay, resetDelay,
additionalContext, additionalContext,
errorPolicyGuard, errorPolicyGuard,
getQueryVariables,
getSubscriptionVariables,
}: DataProviderParams< }: DataProviderParams<
QueryData, QueryData,
Data, Data,
SubscriptionData, SubscriptionData,
Delta, Delta,
Variables Variables,
QueryVariables,
SubscriptionVariables
>): Subscribe<Data, Delta, Variables> { >): Subscribe<Data, Delta, Variables> {
// list of callbacks passed through subscribe call // list of callbacks passed through subscribe call
const callbacks: UpdateCallback<Data, Delta>[] = []; const callbacks: UpdateCallback<Data, Delta>[] = [];
@ -230,7 +242,7 @@ function makeDataProviderInternal<
let loading = true; let loading = true;
let loaded = false; let loaded = false;
let client: ApolloClient<object>; let client: ApolloClient<object>;
let subscription: Subscription | undefined; let subscription: Subscription[] | undefined;
let pageInfo: PageInfo | null = null; let pageInfo: PageInfo | null = null;
let totalCount: number | undefined; let totalCount: number | undefined;
@ -263,7 +275,7 @@ function makeDataProviderInternal<
.query<QueryData>({ .query<QueryData>({
query, query,
variables: { variables: {
...variables, ...(getQueryVariables ? getQueryVariables(variables) : variables),
...(pagination && { ...(pagination && {
// let the variables pagination be prior to provider param // let the variables pagination be prior to provider param
pagination: { pagination: {
@ -343,6 +355,33 @@ function makeDataProviderInternal<
} }
}; };
const subscriptionSubscribe = () => {
if (!subscriptionQuery || !getDelta || !update) {
return;
}
const subscriptionVariables = getSubscriptionVariables
? getSubscriptionVariables(variables)
: variables;
subscription = ([] as (OperationVariables | undefined)[])
.concat(subscriptionVariables)
.map((variables) =>
client
.subscribe<SubscriptionData>({
query: subscriptionQuery,
variables,
fetchPolicy,
})
.subscribe(onNext, onError)
);
};
const subscriptionUnsubscribe = () => {
if (subscription) {
subscription.forEach((subscription) => subscription.unsubscribe());
}
subscription = undefined;
};
const initialFetch = async (isUpdate = false) => { const initialFetch = async (isUpdate = false) => {
if (!client) { if (!client) {
return; return;
@ -392,10 +431,7 @@ function makeDataProviderInternal<
} }
// if error will occur data provider stops subscription // if error will occur data provider stops subscription
error = e as Error; error = e as Error;
if (subscription) { subscriptionUnsubscribe();
subscription.unsubscribe();
}
subscription = undefined;
} finally { } finally {
loading = false; loading = false;
notifyAll({ isUpdate }); notifyAll({ isUpdate });
@ -439,10 +475,7 @@ function makeDataProviderInternal<
const onError = (e: Error) => { const onError = (e: Error) => {
error = e; error = e;
if (subscription) { subscriptionUnsubscribe();
subscription.unsubscribe();
subscription = undefined;
}
notifyAll(); notifyAll();
}; };
@ -460,13 +493,7 @@ function makeDataProviderInternal<
return; return;
} }
if (subscriptionQuery && getDelta && update) { if (subscriptionQuery && getDelta && update) {
subscription = client subscriptionSubscribe();
.subscribe<SubscriptionData>({
query: subscriptionQuery,
variables,
fetchPolicy,
})
.subscribe(onNext, onError);
} }
await initialFetch(); await initialFetch();
}; };
@ -475,8 +502,7 @@ function makeDataProviderInternal<
if (!subscription) { if (!subscription) {
return; return;
} }
subscription.unsubscribe(); subscriptionUnsubscribe();
subscription = undefined;
data = null; data = null;
error = undefined; error = undefined;
loading = false; loading = false;
@ -590,14 +616,18 @@ export function makeDataProvider<
Data, Data,
SubscriptionData, SubscriptionData,
Delta, Delta,
Variables extends OperationVariables | undefined = undefined Variables extends OperationVariables | undefined = undefined,
SubscriptionVariables extends OperationVariables | undefined = Variables,
QueryVariables extends OperationVariables | undefined = Variables
>( >(
params: DataProviderParams< params: DataProviderParams<
QueryData, QueryData,
Data, Data,
SubscriptionData, SubscriptionData,
Delta, Delta,
Variables Variables,
SubscriptionVariables,
QueryVariables
> >
): Subscribe<Data, Delta, Variables> { ): Subscribe<Data, Delta, Variables> {
const getInstance = memoize<Data, Delta, Variables>(() => const getInstance = memoize<Data, Delta, Variables>(() =>

View File

@ -4,41 +4,34 @@ import type { AgGridReact } from 'ag-grid-react';
import type { IsFullWidthRowParams, RowHeightParams } from 'ag-grid-community'; import type { IsFullWidthRowParams, RowHeightParams } from 'ag-grid-community';
const NO_HOVER_CSS_RULE = { 'no-hover': 'data?.isLastPlaceholder' }; const NO_HOVER_CSS_RULE = { 'no-hover': 'data?.isLastPlaceholder' };
const ROW_ID = 'bottomPlaceholder'; const ROW_ID = 'bottom-placeholder';
const fullWidthCellRenderer = () => null; const fullWidthCellRenderer = () => null;
const isFullWidthRow = (params: IsFullWidthRowParams) => const isFullWidthRow = (params: IsFullWidthRowParams) =>
params.rowNode.data?.isLastPlaceholder; params.rowNode.data?.isLastPlaceholder;
interface Props<T> { interface Props {
gridRef: RefObject<AgGridReact>; gridRef: RefObject<AgGridReact>;
setId?: (data: T, id: string) => T;
disabled?: boolean; disabled?: boolean;
} }
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
export const useBottomPlaceholder = <T extends {}>({ export const useBottomPlaceholder = ({ gridRef, disabled }: Props) => {
gridRef,
setId,
disabled,
}: Props<T>) => {
const onBodyScrollEnd = useCallback(() => { const onBodyScrollEnd = useCallback(() => {
const rowCont = gridRef.current?.api.getDisplayedRowCount() ?? 0; const rowCont = gridRef.current?.api.getDisplayedRowCount() ?? 0;
if (rowCont) { if (rowCont) {
const lastRow = gridRef.current?.api.getDisplayedRowAtIndex(rowCont - 1); const lastRow = gridRef.current?.api.getDisplayedRowAtIndex(rowCont - 1);
if (lastRow && lastRow.data) { if (lastRow && lastRow.data) {
const placeholderRow = setId const placeholderRow = {
? setId({ ...lastRow.data, isLastPlaceholder: true }, ROW_ID) ...lastRow.data,
: { isLastPlaceholder: true,
...lastRow.data, id: ROW_ID,
isLastPlaceholder: true, };
id: ROW_ID,
};
const transaction = gridRef.current?.api.getRowNode(ROW_ID) const transaction = gridRef.current?.api.getRowNode(ROW_ID)
? { update: [placeholderRow] } ? { update: [placeholderRow] }
: { add: [placeholderRow] }; : { add: [placeholderRow] };
gridRef.current?.api.applyTransaction(transaction); gridRef.current?.api.applyTransaction(transaction);
} }
} }
}, [gridRef, setId]); }, [gridRef]);
const onRowsChanged = useCallback(() => { const onRowsChanged = useCallback(() => {
const placeholderNode = gridRef.current?.api.getRowNode(ROW_ID); const placeholderNode = gridRef.current?.api.getRowNode(ROW_ID);

View File

@ -5,7 +5,6 @@ import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table'; import { FillsTable } from './fills-table';
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community'; import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
import { useFillsList } from './use-fills-list'; import { useFillsList } from './use-fills-list';
import type { Trade } from './fills-data-provider';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
interface FillsManagerProps { interface FillsManagerProps {
@ -65,7 +64,7 @@ export const FillsManager = ({
}, []); }, []);
const { isFullWidthRow, fullWidthCellRenderer, rowClassRules, getRowHeight } = const { isFullWidthRow, fullWidthCellRenderer, rowClassRules, getRowHeight } =
useBottomPlaceholder<Trade>({ useBottomPlaceholder({
gridRef, gridRef,
}); });

View File

@ -187,7 +187,7 @@ const marketFieldsFragments: MarketFieldsFragment[] = [
name: 'Apple Monthly (30 Jun 2022)', name: 'Apple Monthly (30 Jun 2022)',
product: { product: {
settlementAsset: { settlementAsset: {
id: 'asset-2', id: 'asset-id',
name: '', name: '',
symbol: 'tUSDC', symbol: 'tUSDC',
decimals: 5, decimals: 5,

View File

@ -87,7 +87,7 @@ export const OrderListManager = ({
const { const {
onFilterChanged: bottomPlaceholderOnFilterChanged, onFilterChanged: bottomPlaceholderOnFilterChanged,
...bottomPlaceholderProps ...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({ } = useBottomPlaceholder({
gridRef, gridRef,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder, disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
}); });

View File

@ -9,16 +9,16 @@ fragment PositionFields on Position {
market { market {
id id
} }
party {
id
}
} }
query Positions($partyId: ID!) { query Positions($partyIds: [ID!]!) {
party(id: $partyId) { positions(filter: { partyIds: $partyIds }) {
id edges {
positionsConnection { node {
edges { ...PositionFields
node {
...PositionFields
}
} }
} }
} }
@ -34,6 +34,7 @@ subscription PositionsSubscription($partyId: ID!) {
marketId marketId
lossSocializationAmount lossSocializationAmount
positionStatus positionStatus
partyId
} }
} }

View File

@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } }; export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string }, party: { __typename?: 'Party', id: string } };
export type PositionsQueryVariables = Types.Exact<{ export type PositionsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID']; partyIds: Array<Types.Scalars['ID']> | Types.Scalars['ID'];
}>; }>;
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } } }> | null } | null } | null }; export type PositionsQuery = { __typename?: 'Query', positions?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string }, party: { __typename?: 'Party', id: string } } }> | null } | null };
export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{ export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID']; partyId: Types.Scalars['ID'];
}>; }>;
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus }> }; export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus, partyId: string }> };
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } }; export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
@ -57,6 +57,9 @@ export const PositionFieldsFragmentDoc = gql`
market { market {
id id
} }
party {
id
}
} }
`; `;
export const MarginFieldsFragmentDoc = gql` export const MarginFieldsFragmentDoc = gql`
@ -74,14 +77,11 @@ export const MarginFieldsFragmentDoc = gql`
} }
`; `;
export const PositionsDocument = gql` export const PositionsDocument = gql`
query Positions($partyId: ID!) { query Positions($partyIds: [ID!]!) {
party(id: $partyId) { positions(filter: {partyIds: $partyIds}) {
id edges {
positionsConnection { node {
edges { ...PositionFields
node {
...PositionFields
}
} }
} }
} }
@ -100,7 +100,7 @@ export const PositionsDocument = gql`
* @example * @example
* const { data, loading, error } = usePositionsQuery({ * const { data, loading, error } = usePositionsQuery({
* variables: { * variables: {
* partyId: // value for 'partyId' * partyIds: // value for 'partyIds'
* }, * },
* }); * });
*/ */
@ -126,6 +126,7 @@ export const PositionsSubscriptionDocument = gql`
marketId marketId
lossSocializationAmount lossSocializationAmount
positionStatus positionStatus
partyId
} }
} }
`; `;

View File

@ -7,12 +7,14 @@ export const PositionsContainer = ({
onMarketClick, onMarketClick,
noBottomPlaceholder, noBottomPlaceholder,
storeKey, storeKey,
allKeys,
}: { }: {
onMarketClick?: (marketId: string) => void; onMarketClick?: (marketId: string) => void;
noBottomPlaceholder?: boolean; noBottomPlaceholder?: boolean;
storeKey?: string; storeKey?: string;
allKeys?: boolean;
}) => { }) => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
if (!pubKey) { if (!pubKey) {
return ( return (
@ -21,9 +23,19 @@ export const PositionsContainer = ({
</Splash> </Splash>
); );
} }
const partyIds = [pubKey];
if (allKeys && pubKeys) {
partyIds.push(
...pubKeys
.map(({ publicKey }) => publicKey)
.filter((publicKey) => publicKey !== pubKey)
);
}
return ( return (
<PositionsManager <PositionsManager
partyId={pubKey} partyIds={partyIds}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
noBottomPlaceholder={noBottomPlaceholder} noBottomPlaceholder={noBottomPlaceholder}

View File

@ -1,10 +1,7 @@
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import type { Account } from '@vegaprotocol/accounts'; import type { Account } from '@vegaprotocol/accounts';
import type { MarketWithData } from '@vegaprotocol/markets'; import type { MarketWithData } from '@vegaprotocol/markets';
import type { import type { PositionFieldsFragment } from './__generated__/Positions';
PositionFieldsFragment,
MarginFieldsFragment,
} from './__generated__/Positions';
import { getMetrics, rejoinPositionData } from './positions-data-providers'; import { getMetrics, rejoinPositionData } from './positions-data-providers';
import { PositionStatus } from '@vegaprotocol/types'; import { PositionStatus } from '@vegaprotocol/types';
@ -79,6 +76,9 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market', __typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8', id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
}, },
party: {
id: 'partyId',
},
lossSocializationAmount: '0', lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED, positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
}, },
@ -93,6 +93,9 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market', __typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e', id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
}, },
party: {
id: 'partyId',
},
lossSocializationAmount: '100', lossSocializationAmount: '100',
positionStatus: PositionStatus.POSITION_STATUS_ORDERS_CLOSED, positionStatus: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
}, },
@ -113,6 +116,8 @@ const marketsData = [
product: { product: {
settlementAsset: { settlementAsset: {
symbol: 'tDAI', symbol: 'tDAI',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
}, },
}, },
}, },
@ -140,6 +145,8 @@ const marketsData = [
product: { product: {
settlementAsset: { settlementAsset: {
symbol: 'tDAI', symbol: 'tDAI',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
}, },
}, },
}, },
@ -155,65 +162,23 @@ const marketsData = [
}, },
] as MarketWithData[]; ] as MarketWithData[];
const margins: MarginFieldsFragment[] = [
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
];
describe('getMetrics && rejoinPositionData', () => { describe('getMetrics && rejoinPositionData', () => {
it('returns positions metrics', () => { it('returns positions metrics', () => {
const positionsRejoined = rejoinPositionData( const positionsRejoined = rejoinPositionData(positions, marketsData);
positions,
marketsData,
margins
);
const metrics = getMetrics(positionsRejoined, accounts || null); const metrics = getMetrics(positionsRejoined, accounts || null);
expect(metrics.length).toEqual(2); expect(metrics.length).toEqual(2);
}); });
it('calculates metrics', () => { it('calculates metrics', () => {
const positionsRejoined = rejoinPositionData( const positionsRejoined = rejoinPositionData(positions, marketsData);
positions,
marketsData,
margins
);
const metrics = getMetrics(positionsRejoined, accounts || null); const metrics = getMetrics(positionsRejoined, accounts || null);
expect(metrics[0].assetSymbol).toEqual('tDAI'); expect(metrics[0].assetSymbol).toEqual('tDAI');
expect(metrics[0].averageEntryPrice).toEqual('8993727'); expect(metrics[0].averageEntryPrice).toEqual('8993727');
expect(metrics[0].capitalUtilisation).toEqual(4);
expect(metrics[0].currentLeverage).toBeCloseTo(1.02); expect(metrics[0].currentLeverage).toBeCloseTo(1.02);
expect(metrics[0].marketDecimalPlaces).toEqual(5); expect(metrics[0].marketDecimalPlaces).toEqual(5);
expect(metrics[0].positionDecimalPlaces).toEqual(0); expect(metrics[0].positionDecimalPlaces).toEqual(0);
expect(metrics[0].decimals).toEqual(5); expect(metrics[0].decimals).toEqual(5);
expect(metrics[0].lowMarginLevel).toEqual(false);
expect(metrics[0].markPrice).toEqual('9431775'); expect(metrics[0].markPrice).toEqual('9431775');
expect(metrics[0].marketId).toEqual( expect(metrics[0].marketId).toEqual(
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8' '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
@ -225,7 +190,6 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[0].notional).toEqual('943177500'); expect(metrics[0].notional).toEqual('943177500');
expect(metrics[0].openVolume).toEqual('100'); expect(metrics[0].openVolume).toEqual('100');
expect(metrics[0].realisedPNL).toEqual('0'); expect(metrics[0].realisedPNL).toEqual('0');
expect(metrics[0].searchPrice).toEqual('9098238');
expect(metrics[0].totalBalance).toEqual('926178496'); expect(metrics[0].totalBalance).toEqual('926178496');
expect(metrics[0].unrealisedPNL).toEqual('43804770'); expect(metrics[0].unrealisedPNL).toEqual('43804770');
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z'); expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
@ -236,12 +200,10 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].assetSymbol).toEqual('tDAI'); expect(metrics[1].assetSymbol).toEqual('tDAI');
expect(metrics[1].averageEntryPrice).toEqual('840158'); expect(metrics[1].averageEntryPrice).toEqual('840158');
expect(metrics[1].capitalUtilisation).toEqual(0);
expect(metrics[1].currentLeverage).toBeCloseTo(0.097); expect(metrics[1].currentLeverage).toBeCloseTo(0.097);
expect(metrics[1].marketDecimalPlaces).toEqual(5); expect(metrics[1].marketDecimalPlaces).toEqual(5);
expect(metrics[1].positionDecimalPlaces).toEqual(0); expect(metrics[1].positionDecimalPlaces).toEqual(0);
expect(metrics[1].decimals).toEqual(5); expect(metrics[1].decimals).toEqual(5);
expect(metrics[1].lowMarginLevel).toEqual(false);
expect(metrics[1].markPrice).toEqual('869762'); expect(metrics[1].markPrice).toEqual('869762');
expect(metrics[1].marketId).toEqual( expect(metrics[1].marketId).toEqual(
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e' '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
@ -251,7 +213,6 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].notional).toEqual('86976200'); expect(metrics[1].notional).toEqual('86976200');
expect(metrics[1].openVolume).toEqual('-100'); expect(metrics[1].openVolume).toEqual('-100');
expect(metrics[1].realisedPNL).toEqual('0'); expect(metrics[1].realisedPNL).toEqual('0');
expect(metrics[1].searchPrice).toEqual('902503');
expect(metrics[1].totalBalance).toEqual('896098819'); expect(metrics[1].totalBalance).toEqual('896098819');
expect(metrics[1].unrealisedPNL).toEqual('-9112700'); expect(metrics[1].unrealisedPNL).toEqual('-9112700');
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z'); expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');

View File

@ -19,66 +19,41 @@ import type {
PositionsQuery, PositionsQuery,
PositionFieldsFragment, PositionFieldsFragment,
PositionsSubscriptionSubscription, PositionsSubscriptionSubscription,
MarginFieldsFragment,
PositionsQueryVariables, PositionsQueryVariables,
PositionsSubscriptionSubscriptionVariables,
} from './__generated__/Positions'; } from './__generated__/Positions';
import { import {
PositionsDocument, PositionsDocument,
PositionsSubscriptionDocument, PositionsSubscriptionDocument,
} from './__generated__/Positions'; } from './__generated__/Positions';
import { marginsDataProvider } from './margin-data-provider';
import type { PositionStatus } from '@vegaprotocol/types'; import type { PositionStatus } from '@vegaprotocol/types';
type PositionMarginLevel = Pick<
MarginFieldsFragment,
'maintenanceLevel' | 'searchLevel' | 'initialLevel'
>;
interface PositionRejoined {
realisedPNL: string;
openVolume: string;
unrealisedPNL: string;
averageEntryPrice: string;
updatedAt?: string | null;
market: MarketMaybeWithData | null;
margins: PositionMarginLevel | null;
lossSocializationAmount: string | null;
status: PositionStatus;
}
export interface Position { export interface Position {
marketName: string; assetId: string;
assetSymbol: string;
averageEntryPrice: string; averageEntryPrice: string;
marginAccountBalance: string;
capitalUtilisation: number;
currentLeverage: number | undefined; currentLeverage: number | undefined;
decimals: number; decimals: number;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number; marketDecimalPlaces: number;
positionDecimalPlaces: number;
totalBalance: string;
assetSymbol: string;
assetId: string;
lowMarginLevel: boolean;
marketId: string; marketId: string;
marketName: string;
marketTradingMode: Schema.MarketTradingMode; marketTradingMode: Schema.MarketTradingMode;
markPrice: string | undefined; markPrice: string | undefined;
notional: string | undefined; notional: string | undefined;
openVolume: string; openVolume: string;
partyId: string;
positionDecimalPlaces: number;
realisedPNL: string; realisedPNL: string;
unrealisedPNL: string;
searchPrice: string | undefined;
updatedAt: string | null;
lossSocializationAmount: string;
status: PositionStatus; status: PositionStatus;
} totalBalance: string;
unrealisedPNL: string;
export interface Data { updatedAt: string | null;
party: PositionsQuery['party'] | null;
positions: Position[] | null;
} }
export const getMetrics = ( export const getMetrics = (
data: PositionRejoined[] | null, data: ReturnType<typeof rejoinPositionData> | null,
accounts: Account[] | null accounts: Account[] | null
): Position[] => { ): Position[] => {
if (!data || !data?.length) { if (!data || !data?.length) {
@ -87,25 +62,32 @@ export const getMetrics = (
const metrics: Position[] = []; const metrics: Position[] = [];
data.forEach((position) => { data.forEach((position) => {
const market = position.market; const market = position.market;
if (!market) {
return;
}
const marketData = market?.data; const marketData = market?.data;
const marginLevel = position.margins;
const marginAccount = accounts?.find((account) => { const marginAccount = accounts?.find((account) => {
return account.market?.id === market?.id; return account.market?.id === market?.id;
}); });
if (!marginAccount || !marginLevel || !market) { const {
return; decimals,
} id: assetId,
symbol: assetSymbol,
} = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find( const generalAccount = accounts?.find(
(account) => (account) =>
account.asset.id === marginAccount.asset.id && account.asset.id === assetId &&
account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
); );
const decimals = marginAccount.asset.decimals;
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } = const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
market; market;
const openVolume = toBigNum(position.openVolume, positionDecimalPlaces); const openVolume = toBigNum(position.openVolume, positionDecimalPlaces);
const marginAccountBalance = toBigNum(marginAccount.balance ?? 0, decimals); const marginAccountBalance = toBigNum(
marginAccount?.balance ?? 0,
decimals
);
const generalAccountBalance = toBigNum( const generalAccountBalance = toBigNum(
generalAccount?.balance ?? 0, generalAccount?.balance ?? 0,
decimals decimals
@ -126,54 +108,30 @@ export const getMetrics = (
? new BigNumber(0) ? new BigNumber(0)
: notional.dividedBy(totalBalance) : notional.dividedBy(totalBalance)
: undefined; : undefined;
const capitalUtilisation = totalBalance.isEqualTo(0)
? new BigNumber(0)
: marginAccountBalance.dividedBy(totalBalance).multipliedBy(100);
const marginSearch = toBigNum(marginLevel.searchLevel, decimals);
const marginInitial = toBigNum(marginLevel.initialLevel, decimals);
const searchPrice = markPrice
? marginSearch
.minus(marginAccountBalance)
.dividedBy(openVolume)
.plus(markPrice)
: undefined;
const lowMarginLevel =
marginAccountBalance.isLessThan(
marginSearch.plus(marginInitial.minus(marginSearch).dividedBy(2))
) && generalAccountBalance.isLessThan(marginInitial.minus(marginSearch));
metrics.push({ metrics.push({
marketName: market.tradableInstrument.instrument.name, assetId,
assetSymbol,
averageEntryPrice: position.averageEntryPrice, averageEntryPrice: position.averageEntryPrice,
marginAccountBalance: marginAccount.balance,
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined, currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
marketDecimalPlaces,
positionDecimalPlaces,
decimals, decimals,
assetSymbol: lossSocializationAmount: position.lossSocializationAmount || '0',
market.tradableInstrument.instrument.product.settlementAsset.symbol, marginAccountBalance: marginAccount?.balance ?? '0',
assetId: market.tradableInstrument.instrument.product.settlementAsset.id, marketDecimalPlaces,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
lowMarginLevel,
marketId: market.id, marketId: market.id,
marketName: market.tradableInstrument.instrument.name,
marketTradingMode: market.tradingMode, marketTradingMode: market.tradingMode,
markPrice: marketData ? marketData.markPrice : undefined, markPrice: marketData ? marketData.markPrice : undefined,
notional: notional notional: notional
? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0) ? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
: undefined, : undefined,
openVolume: position.openVolume, openVolume: position.openVolume,
partyId: position.party.id,
positionDecimalPlaces,
realisedPNL: position.realisedPNL, realisedPNL: position.realisedPNL,
status: position.positionStatus,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
unrealisedPNL: position.unrealisedPNL, unrealisedPNL: position.unrealisedPNL,
searchPrice: searchPrice
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
: undefined,
updatedAt: position.updatedAt || null, updatedAt: position.updatedAt || null,
lossSocializationAmount: position.lossSocializationAmount || '0',
status: position.status,
}); });
}); });
return metrics; return metrics;
@ -186,7 +144,8 @@ export const update = (
return produce(data || [], (draft) => { return produce(data || [], (draft) => {
deltas.forEach((delta) => { deltas.forEach((delta) => {
const index = draft.findIndex( const index = draft.findIndex(
(node) => node.market.id === delta.marketId (node) =>
node.market.id === delta.marketId && node.party.id === delta.partyId
); );
if (index !== -1) { if (index !== -1) {
const currNode = draft[index]; const currNode = draft[index];
@ -208,49 +167,39 @@ export const update = (
__typename: 'Market', __typename: 'Market',
id: delta.marketId, id: delta.marketId,
}, },
party: {
id: delta.partyId,
},
}); });
} }
}); });
}); });
}; };
export const positionsDataProvider = makeDataProvider< const getSubscriptionVariables = (
variables: PositionsQueryVariables
): PositionsSubscriptionSubscriptionVariables[] =>
([] as string[]).concat(variables.partyIds).map((partyId) => ({ partyId }));
const positionsDataProvider = makeDataProvider<
PositionsQuery, PositionsQuery,
PositionFieldsFragment[], PositionFieldsFragment[],
PositionsSubscriptionSubscription, PositionsSubscriptionSubscription,
PositionsSubscriptionSubscription['positions'], PositionsSubscriptionSubscription['positions'],
PositionsQueryVariables PositionsQueryVariables,
PositionsSubscriptionSubscriptionVariables
>({ >({
query: PositionsDocument, query: PositionsDocument,
subscriptionQuery: PositionsSubscriptionDocument, subscriptionQuery: PositionsSubscriptionDocument,
update, update,
getData: (responseData: PositionsQuery | null) => getData: (responseData: PositionsQuery | null) =>
removePaginationWrapper(responseData?.party?.positionsConnection?.edges) || removePaginationWrapper(responseData?.positions?.edges) || [],
[],
getDelta: (subscriptionData: PositionsSubscriptionSubscription) => getDelta: (subscriptionData: PositionsSubscriptionSubscription) =>
subscriptionData.positions, subscriptionData.positions,
getSubscriptionVariables,
}); });
const upgradeMarginsConnection = ( const positionDataProvider = makeDerivedDataProvider<
marketId: string,
margins: MarginFieldsFragment[] | null
) => {
if (marketId && margins) {
const index =
margins.findIndex((node) => node.market.id === marketId) ?? -1;
if (index >= 0) {
const marginLevel = margins[index];
return {
maintenanceLevel: marginLevel.maintenanceLevel,
searchLevel: marginLevel.searchLevel,
initialLevel: marginLevel.initialLevel,
};
}
}
return null;
};
export const positionDataProvider = makeDerivedDataProvider<
PositionFieldsFragment, PositionFieldsFragment,
never, never,
PositionsQueryVariables & MarketDataQueryVariables PositionsQueryVariables & MarketDataQueryVariables
@ -258,7 +207,7 @@ export const positionDataProvider = makeDerivedDataProvider<
[ [
(callback, client, variables) => (callback, client, variables) =>
positionsDataProvider(callback, client, { positionsDataProvider(callback, client, {
partyId: variables?.partyId || '', partyIds: variables.partyIds,
}), }),
], ],
(data, variables) => (data, variables) =>
@ -278,22 +227,18 @@ export const openVolumeDataProvider = makeDerivedDataProvider<
export const rejoinPositionData = ( export const rejoinPositionData = (
positions: PositionFieldsFragment[] | null, positions: PositionFieldsFragment[] | null,
marketsData: MarketMaybeWithData[] | null, marketsData: MarketMaybeWithData[] | null
margins: MarginFieldsFragment[] | null ):
): PositionRejoined[] | null => { | (Omit<PositionFieldsFragment, 'market'> & {
if (positions && marketsData && margins) { market: MarketMaybeWithData | null;
})[]
| null => {
if (positions && marketsData) {
return positions.map((node) => { return positions.map((node) => {
return { return {
realisedPNL: node.realisedPNL, ...node,
openVolume: node.openVolume,
unrealisedPNL: node.unrealisedPNL,
averageEntryPrice: node.averageEntryPrice,
updatedAt: node.updatedAt,
market: market:
marketsData?.find((market) => market.id === node.market.id) || null, marketsData?.find((market) => market.id === node.market.id) || null,
margins: upgradeMarginsConnection(node.market.id, margins),
lossSocializationAmount: node.lossSocializationAmount,
status: node.positionStatus,
}; };
}); });
} }
@ -307,13 +252,17 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
>( >(
[ [
positionsDataProvider, positionsDataProvider,
accountsDataProvider, (callback, client, variables) =>
accountsDataProvider(callback, client, {
partyId: Array.isArray(variables.partyIds)
? variables.partyIds[0]
: variables.partyIds,
}),
(callback, client) => (callback, client) =>
allMarketsWithDataProvider(callback, client, undefined), allMarketsWithDataProvider(callback, client, undefined),
marginsDataProvider,
], ],
([positions, accounts, marketsData, margins], variables) => { ([positions, accounts, marketsData], variables) => {
const positionsData = rejoinPositionData(positions, marketsData, margins); const positionsData = rejoinPositionData(positions, marketsData);
if (!variables) { if (!variables) {
return []; return [];
} }

View File

@ -1,6 +1,5 @@
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { Position } from './positions-data-providers';
import { usePositionsData } from './use-positions-data'; import { usePositionsData } from './use-positions-data';
import { PositionsTable } from './positions-table'; import { PositionsTable } from './positions-table';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
@ -8,9 +7,10 @@ import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/wallet'; import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface PositionsManagerProps { interface PositionsManagerProps {
partyId: string; partyIds: string[];
onMarketClick?: (marketId: string) => void; onMarketClick?: (marketId: string) => void;
isReadOnly: boolean; isReadOnly: boolean;
noBottomPlaceholder?: boolean; noBottomPlaceholder?: boolean;
@ -18,14 +18,15 @@ interface PositionsManagerProps {
} }
export const PositionsManager = ({ export const PositionsManager = ({
partyId, partyIds,
onMarketClick, onMarketClick,
isReadOnly, isReadOnly,
noBottomPlaceholder, noBottomPlaceholder,
storeKey, storeKey,
}: PositionsManagerProps) => { }: PositionsManagerProps) => {
const { pubKeys, pubKey } = useVegaWallet();
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading, reload } = usePositionsData(partyId, gridRef); const { data, error, loading, reload } = usePositionsData(partyIds, gridRef);
const [dataCount, setDataCount] = useState(data?.length ?? 0); const [dataCount, setDataCount] = useState(data?.length ?? 0);
const create = useVegaTransactionStore((store) => store.create); const create = useVegaTransactionStore((store) => store.create);
const onClose = ({ const onClose = ({
@ -58,23 +59,19 @@ export const PositionsManager = ({
}, },
}); });
const setId = useCallback((data: Position, id: string) => { const bottomPlaceholderProps = useBottomPlaceholder({
return {
...data,
marketId: id,
};
}, []);
const bottomPlaceholderProps = useBottomPlaceholder<Position>({
gridRef, gridRef,
setId,
disabled: noBottomPlaceholder, disabled: noBottomPlaceholder,
}); });
const updateRowCount = useCallback(() => { const updateRowCount = useCallback(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []); }, []);
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<PositionsTable <PositionsTable
pubKey={pubKey}
pubKeys={pubKeys}
rowData={error ? [] : data} rowData={error ? [] : data}
ref={gridRef} ref={gridRef}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
@ -86,6 +83,7 @@ export const PositionsManager = ({
onRowDataUpdated={updateRowCount} onRowDataUpdated={updateRowCount}
{...bottomPlaceholderProps} {...bottomPlaceholderProps}
storeKey={storeKey} storeKey={storeKey}
multipleKeys={partyIds.length > 1}
/> />
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">
<AsyncRenderer <AsyncRenderer

View File

@ -8,29 +8,27 @@ import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community'; import type { ICellRendererParams } from 'ag-grid-community';
const singleRow: Position = { const singleRow: Position = {
marketName: 'ETH/BTC (31 july 2022)', partyId: 'partyId',
averageEntryPrice: '133',
capitalUtilisation: 11,
currentLeverage: 1.1,
marketDecimalPlaces: 1,
positionDecimalPlaces: 0,
decimals: 2,
totalBalance: '123456',
assetSymbol: 'BTC',
assetId: 'asset-id', assetId: 'asset-id',
lowMarginLevel: false, assetSymbol: 'BTC',
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2,
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
marketId: 'string', marketId: 'string',
marketName: 'ETH/BTC (31 july 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '123', markPrice: '123',
notional: '12300', notional: '12300',
openVolume: '100', openVolume: '100',
positionDecimalPlaces: 0,
realisedPNL: '123', realisedPNL: '123',
unrealisedPNL: '456',
searchPrice: '0',
updatedAt: '2022-07-27T15:02:58.400Z',
marginAccountBalance: '12345600',
status: PositionStatus.POSITION_STATUS_UNSPECIFIED, status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
lossSocializationAmount: '0', totalBalance: '123456',
unrealisedPNL: '456',
updatedAt: '2022-07-27T15:02:58.400Z',
}; };
const singleRowData = [singleRow]; const singleRowData = [singleRow];
@ -175,6 +173,7 @@ it('displays close button', async () => {
render( render(
<PositionsTable <PositionsTable
rowData={singleRowData} rowData={singleRowData}
pubKey={singleRowData[0].partyId}
onClose={() => { onClose={() => {
return; return;
}} }}

View File

@ -15,69 +15,51 @@ const Template: Story = (args) => (
export const Primary = Template.bind({}); export const Primary = Template.bind({});
const longPosition: Position = { const longPosition: Position = {
marketName: 'BTC/USD (31 july 2022)', assetId: 'BTC',
assetSymbol: 'BTC',
averageEntryPrice: '1134564', averageEntryPrice: '1134564',
capitalUtilisation: 10,
currentLeverage: 11, currentLeverage: 11,
decimals: 2, decimals: 2,
marketDecimalPlaces: 2, lossSocializationAmount: '0',
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '45353',
assetSymbol: 'BTC',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
lowMarginLevel: false,
marginAccountBalance: new BigNumber('0').toString(), marginAccountBalance: new BigNumber('0').toString(),
// marginMaintenance: '0', marketDecimalPlaces: 2,
// marginSearch: '0',
// marginInitial: '0',
marketId: 'marketId1', marketId: 'marketId1',
marketName: 'BTC/USD (31 july 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '1131894', markPrice: '1131894',
notional: '46667989', notional: '46667989',
openVolume: '4123', openVolume: '4123',
positionDecimalPlaces: 2,
realisedPNL: '45', realisedPNL: '45',
unrealisedPNL: '45',
searchPrice: '1132123',
updatedAt: '2022-07-27T15:02:58.400Z',
lossSocializationAmount: '0',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED, status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
totalBalance: '45353',
unrealisedPNL: '45',
updatedAt: '2022-07-27T15:02:58.400Z',
partyId: 'partyId',
}; };
const shortPosition: Position = { const shortPosition: Position = {
marketName: 'ETH/USD (31 august 2022)', assetId: 'ETH',
assetSymbol: 'ETH',
averageEntryPrice: '23976', averageEntryPrice: '23976',
capitalUtilisation: 87,
currentLeverage: 7, currentLeverage: 7,
decimals: 2, decimals: 2,
marketDecimalPlaces: 2, lossSocializationAmount: '0',
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '3856',
assetSymbol: 'ETH',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
lowMarginLevel: false,
marginAccountBalance: new BigNumber('0').toString(), marginAccountBalance: new BigNumber('0').toString(),
// marginMaintenance: '0', marketDecimalPlaces: 2,
// marginSearch: '0',
// marginInitial: '0',
marketId: 'marketId2', marketId: 'marketId2',
marketName: 'ETH/USD (31 august 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '24123', markPrice: '24123',
notional: '836344', notional: '836344',
openVolume: '-3467', openVolume: '-3467',
positionDecimalPlaces: 2,
realisedPNL: '0', realisedPNL: '0',
unrealisedPNL: '0',
searchPrice: '0',
updatedAt: '2022-07-26T14:01:34.800Z',
lossSocializationAmount: '0',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED, status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
totalBalance: '3856',
unrealisedPNL: '0',
updatedAt: '2022-07-26T14:01:34.800Z',
partyId: 'partyId',
}; };
Primary.args = { Primary.args = {

View File

@ -42,6 +42,7 @@ import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment'; import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown'; import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
interface Props extends TypedDataAgGrid<Position> { interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void; onClose?: (data: Position) => void;
@ -49,6 +50,9 @@ interface Props extends TypedDataAgGrid<Position> {
style?: CSSProperties; style?: CSSProperties;
isReadOnly: boolean; isReadOnly: boolean;
storeKey?: string; storeKey?: string;
multipleKeys?: boolean;
pubKeys?: VegaWalletContextShape['pubKeys'];
pubKey?: VegaWalletContextShape['pubKey'];
} }
export interface AmountCellProps { export interface AmountCellProps {
@ -83,7 +87,18 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
AmountCell.displayName = 'AmountCell'; AmountCell.displayName = 'AmountCell';
export const PositionsTable = forwardRef<AgGridReact, Props>( export const PositionsTable = forwardRef<AgGridReact, Props>(
({ onClose, onMarketClick, ...props }, ref) => { (
{
onClose,
onMarketClick,
multipleKeys,
isReadOnly,
pubKeys,
pubKey,
...props
},
ref
) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return ( return (
<AgGrid <AgGrid
@ -107,6 +122,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}} }}
{...props} {...props}
> >
{multipleKeys ? (
<AgGridColumn
headerName={t('Vega key')}
field="partyId"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'partyId'>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)?.name) ||
data?.partyId
}
minWidth={190}
/>
) : null}
<AgGridColumn <AgGridColumn
headerName={t('Market')} headerName={t('Market')}
field="marketName" field="marketName"
@ -267,55 +297,61 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}} }}
minWidth={100} minWidth={100}
/> />
<AgGridColumn {multipleKeys ? null : (
headerName={t('Leverage')} <AgGridColumn
field="currentLeverage" headerName={t('Leverage')}
type="rightAligned" field="currentLeverage"
filter="agNumberColumnFilter" type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => { filter="agNumberColumnFilter"
return { cellRendererSelector={(): CellRendererSelectorResult => {
component: PriceFlashCell, return {
}; component: PriceFlashCell,
}} };
valueFormatter={({ }}
value, valueFormatter={({
}: VegaValueFormatterParams<Position, 'currentLeverage'>) => value,
value === undefined ? undefined : formatNumber(value.toString(), 1) }: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
} value === undefined
minWidth={100} ? undefined
/> : formatNumber(value.toString(), 1)
<AgGridColumn
headerName={t('Margin allocated')}
field="marginAccountBalance"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data
? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
if (!data) {
return undefined;
} }
return addDecimalsFormatNumber( minWidth={100}
data.marginAccountBalance, />
data.decimals )}
); {multipleKeys ? null : (
}} <AgGridColumn
minWidth={100} headerName={t('Margin allocated')}
/> field="marginAccountBalance"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data
? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
}}
minWidth={100}
/>
)}
<AgGridColumn <AgGridColumn
headerName={t('Realised PNL')} headerName={t('Realised PNL')}
field="realisedPNL" field="realisedPNL"
@ -385,13 +421,15 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}} }}
minWidth={150} minWidth={150}
/> />
{onClose && !props.isReadOnly ? ( {onClose && !isReadOnly ? (
<AgGridColumn <AgGridColumn
{...COL_DEFS.actions} {...COL_DEFS.actions}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => { cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
return ( return (
<div className="flex gap-2 items-center justify-end"> <div className="flex gap-2 items-center justify-end">
{data?.openVolume && data?.openVolume !== '0' ? ( {data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink <ButtonLink
data-testid="close-position" data-testid="close-position"
onClick={() => data && onClose(data)} onClick={() => data && onClose(data)}

View File

@ -12,16 +12,12 @@ export const positionsQuery = (
override?: PartialDeep<PositionsQuery> override?: PartialDeep<PositionsQuery>
): PositionsQuery => { ): PositionsQuery => {
const defaultResult: PositionsQuery = { const defaultResult: PositionsQuery = {
party: { positions: {
__typename: 'Party', __typename: 'PositionConnection',
id: 'vega-0', // VEGA PUBLIC KEY edges: positionFields.map((node) => ({
positionsConnection: { __typename: 'PositionEdge',
__typename: 'PositionConnection', node,
edges: positionFields.map((node) => ({ })),
__typename: 'PositionEdge',
node,
})),
},
}, },
}; };
@ -59,6 +55,10 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-0', id: 'market-0',
__typename: 'Market', __typename: 'Market',
}, },
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0', lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED, positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
}, },
@ -73,6 +73,10 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-1', id: 'market-1',
__typename: 'Market', __typename: 'Market',
}, },
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0', lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED, positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
}, },
@ -87,6 +91,10 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-2', id: 'market-2',
__typename: 'Market', __typename: 'Market',
}, },
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0', lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED, positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
}, },

View File

@ -14,7 +14,7 @@ export const useOpenVolume = (
useDataProvider({ useDataProvider({
dataProvider: openVolumeDataProvider, dataProvider: openVolumeDataProvider,
update, update,
variables: { partyId: partyId || '', marketId }, variables: { partyIds: partyId ? [partyId] : [], marketId },
skip: !partyId, skip: !partyId,
}); });
return openVolume; return openVolume;

View File

@ -59,7 +59,7 @@ describe('usePositionData Hook', () => {
.mockImplementation((id: string) => .mockImplementation((id: string) =>
mockData.find((position) => position.marketId === id) mockData.find((position) => position.marketId === id)
); );
const partyId = 'partyId'; const partyIds = ['partyId'];
const anUpdatedOne = { const anUpdatedOne = {
marketId: 'market-1', marketId: 'market-1',
openVolume: '1', openVolume: '1',
@ -75,14 +75,14 @@ describe('usePositionData Hook', () => {
}; };
it('should return proper data', async () => { it('should return proper data', async () => {
const { result } = renderHook(() => usePositionsData(partyId, gridRef), { const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider, wrapper: MockedProvider,
}); });
expect(result.current.data?.length ?? 0).toEqual(5); expect(result.current.data?.length ?? 0).toEqual(5);
}); });
it('should call mockRefreshInfiniteCache', async () => { it('should call mockRefreshInfiniteCache', async () => {
renderHook(() => usePositionsData(partyId, gridRef), { renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider, wrapper: MockedProvider,
}); });
await waitFor(() => { await waitFor(() => {
@ -99,7 +99,7 @@ describe('usePositionData Hook', () => {
data: mockData, data: mockData,
loading: false, loading: false,
}; };
const { result } = renderHook(() => usePositionsData(partyId, gridRef), { const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider, wrapper: MockedProvider,
}); });
expect(result.current.data).toEqual([]); expect(result.current.data).toEqual([]);

View File

@ -9,15 +9,22 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
import type { GetRowsParams } from '@vegaprotocol/datagrid'; import type { GetRowsParams } from '@vegaprotocol/datagrid';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
export const getRowId = ({ data }: { data: Position }) => data.marketId; export const getRowId = ({
data,
}: {
data: Position & { isLastPlaceholder?: boolean; id?: string };
}) =>
data.isLastPlaceholder && data.id
? data.id
: `${data.partyId}-${data.marketId}`;
export const usePositionsData = ( export const usePositionsData = (
partyId: string, partyIds: string[],
gridRef: RefObject<AgGridReact> gridRef: RefObject<AgGridReact>
) => { ) => {
const variables = useMemo<PositionsQueryVariables>( const variables = useMemo<PositionsQueryVariables>(
() => ({ partyId }), () => ({ partyIds }),
[partyId] [partyIds]
); );
const dataRef = useRef<Position[] | null>(null); const dataRef = useRef<Position[] | null>(null);
const update = useCallback( const update = useCallback(
@ -28,14 +35,16 @@ export const usePositionsData = (
const update: Position[] = []; const update: Position[] = [];
const add: Position[] = []; const add: Position[] = [];
data?.forEach((d) => { data?.forEach((row) => {
const rowNode = gridRef.current?.api?.getRowNode(d.marketId); const rowNode = gridRef.current?.api?.getRowNode(
getRowId({ data: row })
);
if (rowNode) { if (rowNode) {
if (!isEqual(rowNode.data, d)) { if (!isEqual(rowNode.data, row)) {
update.push(d); update.push(row);
} }
} else { } else {
add.push(d); add.push(row);
} }
}); });
gridRef.current?.api?.applyTransaction({ gridRef.current?.api?.applyTransaction({