Compare commits

..
49 changed files with 516 additions and 1133 deletions
@@ -39,6 +39,7 @@ context('Market page', { tags: '@regression' }, function () {
cy.contains('Test market 1').click();
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
cy.validate_element_from_table('Name', 'Test market 1');
cy.validate_element_from_table('Market ID', this.createdMarketId);
cy.validate_element_from_table('Trading Mode', 'Opening auction');
cy.validate_element_from_table('Market Decimal Places', '5');
cy.validate_element_from_table('Position Decimal Places', '5');
@@ -537,10 +537,10 @@ describe(
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
cy.reload();
getProposalFromTitle('Governance transfer proposal').within(() => {
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
cy.getByTestId(governanceTransferToggle).click();
cy.getByTestId('proposal-transfer-details-table').within(() => {
getProposalInformationFromTable('Source Type')
@@ -590,7 +590,7 @@ describe(
);
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(marketProposalType).should('have.text', 'Cancel transfer');
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
getProposalInformationFromTable('Error details')
.invoke('text')
.and('eq', 'Governance transfer invalid transfer id not found');
@@ -28,10 +28,9 @@ const headers = [
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Fees accrued this epoch',
'Live time on book',
'Live time fraction on book',
'Live liquidity quality score (%)',
'Last time on the book',
'Last time fraction on the book',
'Last fee penalty',
'Last bond penalty',
'Status',
+8 -4
View File
@@ -3,16 +3,17 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
@@ -25,3 +26,6 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
@@ -18,7 +18,6 @@ import { Routes } from '../../lib/links';
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
import { t } from '@vegaprotocol/i18n';
import { Statistics } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program';
const RELOAD_DELAY = 3000;
@@ -33,7 +32,6 @@ const validateCode = (value: string) => {
};
export const ApplyCodeForm = () => {
const program = useReferralProgram();
const navigate = useNavigate();
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
@@ -239,7 +237,7 @@ export const ApplyCodeForm = () => {
{previewData ? (
<div className="mt-10">
<h2 className="text-2xl mb-5">{t('You are joining')}</h2>
<Statistics data={previewData} program={program} as="referee" />
<Statistics data={previewData} as="referee" />
</div>
) : null}
</>
@@ -1,5 +1,5 @@
query Referees($code: ID!, $aggregationEpochs: Int) {
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
query Referees($code: ID!, $aggregationDays: Int) {
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
edges {
node {
referralSetId
@@ -10,7 +10,6 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
referralSetRunningNotionalTakerVolume
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
}
}
}
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type RefereesQueryVariables = Types.Exact<{
code: Types.Scalars['ID'];
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
aggregationDays?: Types.InputMaybe<Types.Scalars['Int']>;
}>;
@@ -13,8 +13,8 @@ export type RefereesQuery = { __typename?: 'Query', referralSetReferees: { __typ
export const RefereesDocument = gql`
query Referees($code: ID!, $aggregationEpochs: Int) {
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
query Referees($code: ID!, $aggregationDays: Int) {
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
edges {
node {
referralSetId
@@ -42,7 +42,7 @@ export const RefereesDocument = gql`
* const { data, loading, error } = useRefereesQuery({
* variables: {
* code: // value for 'code'
* aggregationEpochs: // value for 'aggregationEpochs'
* aggregationDays: // value for 'aggregationDays'
* },
* });
*/
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
}>;
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string } } | null> } };
export const ReferralSetStatsDocument = gql`
@@ -25,7 +25,6 @@ export const ReferralSetStatsDocument = gql`
referralSetRunningNotionalTakerVolume
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
}
}
}
@@ -5,14 +5,14 @@ import compact from 'lodash/compact';
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
export const DEFAULT_AGGREGATION_DAYS = 30;
const DEFAULT_AGGREGATION_DAYS = 30;
export type Role = 'referrer' | 'referee';
type UseReferralArgs = (
| { code: string }
| { pubKey: string | null; role: Role }
) & {
aggregationEpochs?: number;
aggregationDays?: number;
};
const prepareVariables = (
@@ -70,9 +70,9 @@ export const useReferral = (args: UseReferralArgs) => {
} = useRefereesQuery({
variables: {
code: referralSet?.id as string,
aggregationEpochs:
args.aggregationEpochs != null
? args.aggregationEpochs
aggregationDays:
args.aggregationDays != null
? args.aggregationDays
: DEFAULT_AGGREGATION_DAYS,
},
skip: !referralSet?.id,
@@ -6,7 +6,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
import { useReferral } from './hooks/use-referral';
import { CreateCodeContainer } from './create-code-form';
import classNames from 'classnames';
import { Table } from './table';
@@ -27,30 +27,25 @@ import { useLayoutEffect, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import { t } from '@vegaprotocol/i18n';
import maxBy from 'lodash/maxBy';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
const program = useReferralProgram();
const { data: referee } = useReferral({
pubKey,
role: 'referee',
aggregationEpochs: program.details?.windowLength,
});
const { data: referrer } = useReferral({
pubKey,
role: 'referrer',
aggregationEpochs: program.details?.windowLength,
});
if (referee?.code) {
return <Statistics data={referee} program={program} as="referee" />;
return <Statistics data={referee} as="referee" />;
}
if (referrer?.code) {
return <Statistics data={referrer} program={program} as="referrer" />;
return <Statistics data={referrer} as="referrer" />;
}
return <CreateCodeContainer />;
@@ -58,16 +53,14 @@ export const ReferralStatistics = () => {
export const Statistics = ({
data,
program,
as,
}: {
data: NonNullable<ReturnType<typeof useReferral>['data']>;
program: ReturnType<typeof useReferralProgram>;
as: 'referrer' | 'referee';
}) => {
const { benefitTiers, details } = program;
const { data: epochData } = useCurrentEpochInfoQuery();
const { stakeAvailable } = useStakeAvailable();
const { benefitTiers } = useReferralProgram();
const { data: statsData } = useReferralSetStatsQuery({
variables: {
code: data.code,
@@ -78,13 +71,6 @@ export const Statistics = ({
const currentEpoch = Number(epochData?.epoch.id);
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
notation: 'compact',
compactDisplay: 'short',
});
const stats =
statsData?.referralSetStats.edges &&
compact(removePaginationWrapper(statsData.referralSetStats.edges));
@@ -100,13 +86,10 @@ export const Statistics = ({
const runningVolumeValue = statsAvailable
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
: 0;
const referrerVolumeValue = statsAvailable
? Number(statsAvailable.referrerTakerVolume)
: 0;
const multiplier = statsAvailable
? Number(statsAvailable.rewardsMultiplier)
: 1;
const finalCommissionValue = isNaN(multiplier)
const finalCommissionValue = !isNaN(multiplier)
? baseCommissionValue
: multiplier * baseCommissionValue;
@@ -119,9 +102,9 @@ export const Statistics = ({
!isNaN(t.discountFactor) &&
t.discountFactor === discountFactorValue
);
const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
const nextBenefitTierValue =
currentBenefitTierValue &&
benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1);
const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch
? currentEpoch - refereeInfo?.atEpoch
@@ -134,13 +117,7 @@ export const Statistics = ({
: 0;
const baseCommissionTile = (
<StatTile
title={t('Base commission rate')}
description={t('(Combined set volume %s over last %s epochs)', [
compactNumFormat.format(runningVolumeValue),
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString(),
])}
>
<StatTile title={t('Base commission rate')}>
{baseCommissionValue * 100}%
</StatTile>
);
@@ -156,16 +133,7 @@ export const Statistics = ({
</StatTile>
);
const finalCommissionTile = (
<StatTile
title={t('Final commission rate')}
description={
!isNaN(multiplier)
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
finalCommissionValue * 100
}%)`
: undefined
}
>
<StatTile title={t('Final commission rate')}>
{finalCommissionValue * 100}%
</StatTile>
);
@@ -174,21 +142,12 @@ export const Statistics = ({
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
);
const codeTile = (
<CodeTile
code={data?.code}
createdAt={getDateFormat().format(new Date(data.createdAt))}
/>
);
const referrerVolumeTile = (
<StatTile
title={t(
'My volume (last %s epochs)',
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
)}
>
{compactNumFormat.format(referrerVolumeValue)}
const codeTile = <CodeTile code={data?.code} />;
const createdAtTile = (
<StatTile title={t('Created at')}>
<span className="text-3xl">
{getDateFormat().format(new Date(data.createdAt))}
</span>
</StatTile>
);
@@ -197,10 +156,7 @@ export const Statistics = ({
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile
title={t(
'Total commission (last %s epochs)',
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
)}
title={t('Total commission (last 30 days)')}
description={t('(qUSD)')}
>
{getNumberFormat(0).format(Number(totalCommissionValue))}
@@ -217,16 +173,23 @@ export const Statistics = ({
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
{codeTile}
{referrerVolumeTile}
{createdAtTile}
{numberOfTradersTile}
{totalCommissionTile}
</div>
</>
);
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
notation: 'compact',
compactDisplay: 'short',
});
const currentBenefitTierTile = (
<StatTile title={t('Current tier')}>
{currentBenefitTierValue?.tier || 'None'}
{currentBenefitTierValue?.tier || '-'}
</StatTile>
);
const discountFactorTile = (
@@ -241,24 +204,14 @@ export const Statistics = ({
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
);
const nextTierVolumeTile = (
<StatTile
title={t(
'Volume to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
<StatTile title={t('Volume to next tier')}>
{nextBenefitTierVolumeValue <= 0
? '0'
: compactNumFormat.format(nextBenefitTierVolumeValue)}
</StatTile>
);
const nextTierEpochsTile = (
<StatTile
title={t(
'Epochs to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
<StatTile title={t('Epochs to next tier')}>
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
</StatTile>
);
@@ -302,7 +255,7 @@ export const Statistics = ({
{/* Referees (only for referrer view) */}
{as === 'referrer' && data.referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
<h2 className="text-2xl mb-5">{t('Referees')}</h2>
<div
className={classNames(
collapsed && [
@@ -328,23 +281,10 @@ export const Statistics = ({
columns={[
{ name: 'party', displayName: t('Trader') },
{ name: 'joined', displayName: t('Date Joined') },
{
name: 'volume',
displayName: t(
'Volume (last %s epochs)',
(
details?.windowLength || DEFAULT_AGGREGATION_DAYS
).toString()
),
},
{ name: 'volume', displayName: t('Volume (last 30 days)') },
{
name: 'commission',
displayName: t(
'Commission earned (last %s epochs)',
(
details?.windowLength || DEFAULT_AGGREGATION_DAYS
).toString()
),
displayName: t('Commission earned (last 30 days)'),
},
]}
data={sortBy(
+1 -12
View File
@@ -109,7 +109,6 @@ export const TiersContainer = () => {
<Loading variant="large" />
) : (
<TiersTable
windowLength={details?.windowLength}
data={benefitTiers.map((bt) => ({
...bt,
tierElement: (
@@ -163,7 +162,6 @@ const StakingTiers = ({
const TiersTable = ({
data,
windowLength,
}: {
data: Array<{
tier: number;
@@ -172,7 +170,6 @@ const TiersTable = ({
discount: string;
volume: string;
}>;
windowLength?: number;
}) => {
return (
<Table
@@ -184,15 +181,7 @@ const TiersTable = ({
tooltip: t('A percentage of commission earned by the referrer'),
},
{ name: 'discount', displayName: t('Referrer trading discount') },
{
name: 'volume',
displayName: t(
'Min. trading volume %s',
windowLength
? t('(last %s epochs)', windowLength.toString())
: undefined
),
},
{ name: 'volume', displayName: t('Min. trading volume') },
{ name: 'epochs', displayName: t('Min. epochs') },
]}
data={data.map((d) => ({
+2 -8
View File
@@ -7,7 +7,6 @@ import {
import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import { Button } from './buttons';
import { t } from '@vegaprotocol/i18n';
export const Tile = ({
className,
@@ -55,18 +54,13 @@ const FADE_OUT_STYLE = classNames(
export const CodeTile = ({
code,
createdAt,
className,
}: {
code: string;
createdAt?: string;
className?: string;
}) => {
return (
<StatTile
title={t('Your referral code')}
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
>
<StatTile title="Your referral code">
<div className="flex gap-2 items-center justify-between">
<Tooltip
description={
@@ -88,7 +82,7 @@ export const CodeTile = ({
</Tooltip>
<CopyWithTooltip text={code}>
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
<span className="sr-only">{t('Copy')}</span>
<span className="sr-only">Copy</span>
<VegaIcon size={24} name={VegaIconNames.COPY} />
</Button>
</CopyWithTooltip>
+2 -2
View File
@@ -1,5 +1,5 @@
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { useProposalToasts } from '@vegaprotocol/proposals';
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
import { Links } from '../lib/links';
export const ToastsManager = () => {
useProposalToasts();
useUpdateNetworkParametersToasts();
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
@@ -1,83 +1,31 @@
import { render, screen, within } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CandlesMenu } from './candles-menu';
import {
useCandlesChartSettingsStore,
DEFAULT_CHART_SETTINGS,
} from './use-candles-chart-settings';
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
describe('CandlesMenu', () => {
const openDropdown = async () => {
await userEvent.click(
screen.getByRole('button', {
name: 'Indicators',
})
);
};
beforeEach(() => {
// clear store each time to avoid conditional testing of defaults
useCandlesChartSettingsStore.setState({ overlays: [], studies: [] });
});
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
render(<CandlesMenu />);
await openDropdown();
const menu = within(await screen.findByRole('menu'));
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
// re-open the dropdown
await openDropdown();
expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute(
'data-state',
'checked'
);
});
it.each(Object.values(Study))('can set %s study', async (study) => {
render(<CandlesMenu />);
await openDropdown();
const menu = within(await screen.findByRole('menu'));
await userEvent.click(menu.getByText(studyLabels[study as Study]));
// re-open the dropdown
await openDropdown();
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
'data-state',
'checked'
);
});
it('should render with the correct default studies and overlays', async () => {
useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS);
it('should render with the correct default studies', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Indicators',
name: 'Studies',
})
);
const menu = within(await screen.findByRole('menu'));
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
});
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
'data-state',
'checked'
it('should render with the correct default overlays', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Overlays',
})
);
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
'data-state',
'checked'
);
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Moving average')).toHaveAttribute(
'data-state',
'checked'
);
+13 -1
View File
@@ -107,7 +107,7 @@ export const CandlesMenu = () => {
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Indicators')}
{t('Overlays')}
</TradingButton>
</TradingDropdownTrigger>
}
@@ -132,6 +132,18 @@ export const CandlesMenu = () => {
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Studies')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
{Object.values(Study).map((study) => (
<TradingDropdownCheckboxItem
key={study}
@@ -24,7 +24,7 @@ const STUDY_ORDER: Study[] = [
Study.VOLUME,
];
export const DEFAULT_CHART_SETTINGS = {
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
+10 -27
View File
@@ -3,12 +3,7 @@ import throttle from 'lodash/throttle';
import isEqualWith from 'lodash/isEqualWith';
import { useApolloClient } from '@apollo/client';
import type { OperationVariables } from '@apollo/client';
import type {
Subscribe,
Load,
UpdateCallback,
PageInfo,
} from './generic-data-provider';
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
import { variablesIsEqualCustomizer } from './generic-data-provider';
export interface useDataProviderParams<
@@ -17,23 +12,13 @@ export interface useDataProviderParams<
Variables extends OperationVariables | undefined = undefined
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({
delta,
data,
pageInfo,
}: {
delta?: Delta;
data: Data | null;
pageInfo: PageInfo | null;
}) => boolean;
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
insert?: ({
insertionData,
data,
pageInfo,
}: {
insertionData?: Data | null;
data: Data | null;
pageInfo: PageInfo | null;
}) => boolean;
variables: Variables;
skipUpdates?: boolean;
@@ -45,7 +30,7 @@ export interface useDataProviderParams<
* @param dataProvider subscribe function created by makeDataProvider
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
* @param variables optional
* @returns state: data, loading, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/
export const useDataProvider = <
Data,
@@ -63,7 +48,6 @@ export const useDataProvider = <
const [data, setData] = useState<Data | null>(null);
const [loading, setLoading] = useState<boolean>(!skip);
const [error, setError] = useState<Error | undefined>(undefined);
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
const flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined);
@@ -109,7 +93,6 @@ export const useDataProvider = <
isInsert,
isUpdate,
loaded,
pageInfo,
} = args;
setError(error);
setLoading(!loaded && loading);
@@ -121,22 +104,21 @@ export const useDataProvider = <
(skipUpdatesRef.current ||
(!skipUpdatesRef.current &&
updateRef.current &&
updateRef.current({ delta, data, pageInfo })))
updateRef.current({ delta, data })))
) {
return;
}
if (
isInsert &&
insertRef.current &&
insertRef.current({ insertionData, data, pageInfo })
insertRef.current({ insertionData, data })
) {
return;
}
}
setData(data);
setPageInfo(pageInfo);
if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data, pageInfo });
updateRef.current({ data });
}
}, []);
@@ -154,13 +136,15 @@ export const useDataProvider = <
useEffect(() => {
setData(null);
setPageInfo(null);
setError(undefined);
if (updateRef.current) {
updateRef.current({ data: null, pageInfo: null });
updateRef.current({ data: null });
}
if (skip) {
setLoading(false);
if (updateRef.current) {
updateRef.current({ data: null });
}
return;
}
setLoading(true);
@@ -181,7 +165,6 @@ export const useDataProvider = <
}, [client, dataProvider, callback, variables, skip]);
return {
data,
pageInfo,
loading,
error,
flush,
@@ -109,7 +109,7 @@ describe('DealTicket', () => {
variables: {
partyId: 'pubKey',
filter: { liveOnly: true },
pagination: { first: 1000 },
pagination: { first: 5000 },
},
},
result: {
+2 -6
View File
@@ -42,12 +42,8 @@ fragment FillEdge on TradeEdge {
cursor
}
query Fills(
$filter: TradesFilter
$pagination: Pagination
$dateRange: DateRange
) {
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
edges {
...FillEdge
}
+2 -4
View File
@@ -12,7 +12,6 @@ export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node:
export type FillsQueryVariables = Types.Exact<{
filter?: Types.InputMaybe<Types.TradesFilter>;
pagination?: Types.InputMaybe<Types.Pagination>;
dateRange?: Types.InputMaybe<Types.DateRange>;
}>;
@@ -96,8 +95,8 @@ export const FillUpdateFieldsFragmentDoc = gql`
}
${TradeFeeFieldsFragmentDoc}`;
export const FillsDocument = gql`
query Fills($filter: TradesFilter, $pagination: Pagination, $dateRange: DateRange) {
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
edges {
...FillEdge
}
@@ -125,7 +124,6 @@ export const FillsDocument = gql`
* variables: {
* filter: // value for 'filter'
* pagination: // value for 'pagination'
* dateRange: // value for 'dateRange'
* },
* });
*/
+18 -53
View File
@@ -1,12 +1,11 @@
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState } from 'react';
import { useRef } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import { fillsWithMarketProvider } from './fills-data-provider';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface FillsManagerProps {
partyId: string;
@@ -23,60 +22,26 @@ export const FillsManager = ({
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
partyIds: [partyId],
};
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
const { data, error, load, pageInfo } = useDataProvider({
const { data, error } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update: ({ data }) => {
if (data?.length && gridRef.current?.api) {
gridRef.current?.api.setRowData(data);
return true;
}
return false;
},
variables: { filter },
});
return (
<div className="flex flex-col h-full">
<FillsTable
ref={gridRef}
rowData={data}
onFilterChanged={(event) => {
onRowDataUpdated(event);
onFilterChanged(event);
}}
onRowDataUpdated={onRowDataUpdated}
partyId={partyId}
onMarketClick={onMarketClick}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No fills matching selected filters')}
</div>
) : null}
</div>
</div>
return (
<FillsTable
ref={gridRef}
rowData={data}
partyId={partyId}
onMarketClick={onMarketClick}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
{...gridProps}
/>
);
};
-2
View File
@@ -20,7 +20,6 @@ import {
negativeClassNames,
MarketNameCell,
COL_DEFS,
DateRangeFilter,
} from '@vegaprotocol/datagrid';
import type {
VegaValueFormatterParams,
@@ -121,7 +120,6 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
},
{
headerName: t('Date'),
filter: DateRangeFilter,
field: 'createdAt',
valueFormatter: ({
value,
+3 -14
View File
@@ -4,7 +4,7 @@ fragment LiquidityProvisionFields on LiquidityProvision {
id
party {
id
accountsConnection(marketId: $marketId) {
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
edges {
node {
type
@@ -22,21 +22,10 @@ fragment LiquidityProvisionFields on LiquidityProvision {
query LiquidityProvisions($marketId: ID!) {
market(id: $marketId) {
liquiditySLAParameters {
priceRange
commitmentMinTimeFraction
performanceHysteresisEpochs
slaCompetitionFactor
}
liquidityProvisions(live: true) {
liquidityProvisionsConnection(live: true) {
edges {
node {
current {
...LiquidityProvisionFields
}
pending {
...LiquidityProvisionFields
}
...LiquidityProvisionFields
}
}
}
@@ -1,12 +1,10 @@
fragment MarketNode on Market {
id
liquidityProvisions(live: true) {
liquidityProvisionsConnection(live: true) {
edges {
node {
current {
commitmentAmount
fee
}
commitmentAmount
fee
}
}
}
+4 -15
View File
@@ -10,7 +10,7 @@ export type LiquidityProvisionsQueryVariables = Types.Exact<{
}>;
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquiditySLAParameters?: { __typename?: 'LiquiditySLAParameters', priceRange: string, commitmentMinTimeFraction: string, performanceHysteresisEpochs: number, slaCompetitionFactor: string } | null, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } }, pending?: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } | null } } | null> | null } | null } | null };
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
export type LiquidityProviderFeeShareFieldsFragment = { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, averageScore: string, virtualStake: string };
@@ -30,7 +30,7 @@ export const LiquidityProvisionFieldsFragmentDoc = gql`
id
party {
id
accountsConnection(marketId: $marketId) {
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
edges {
node {
type
@@ -82,21 +82,10 @@ ${LiquidityProviderSLAFieldsFragmentDoc}`;
export const LiquidityProvisionsDocument = gql`
query LiquidityProvisions($marketId: ID!) {
market(id: $marketId) {
liquiditySLAParameters {
priceRange
commitmentMinTimeFraction
performanceHysteresisEpochs
slaCompetitionFactor
}
liquidityProvisions(live: true) {
liquidityProvisionsConnection(live: true) {
edges {
node {
current {
...LiquidityProvisionFields
}
pending {
...LiquidityProvisionFields
}
...LiquidityProvisionFields
}
}
}
+5 -7
View File
@@ -3,23 +3,21 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
export type LiquidityProvisionMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
export const MarketNodeFragmentDoc = gql`
fragment MarketNode on Market {
id
liquidityProvisions(live: true) {
liquidityProvisionsConnection(live: true) {
edges {
node {
current {
commitmentAmount
fee
}
commitmentAmount
fee
}
}
}
@@ -1,12 +1,13 @@
import { AccountType } from '@vegaprotocol/types';
import type { LiquidityProvisionFields } from './liquidity-data-provider';
import { getLiquidityProvision } from './liquidity-data-provider';
import type { LiquidityProviderFieldsFragment } from './__generated__/MarketLiquidity';
import type {
LiquidityProviderFieldsFragment,
LiquidityProvisionFieldsFragment,
} from './__generated__/MarketLiquidity';
const input = {
liquidityProvisions: [
{
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
party: {
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
accountsConnection: {
@@ -30,11 +31,7 @@ const input = {
fee: '0.001',
status: 'STATUS_ACTIVE',
__typename: 'LiquidityProvision',
priceRange: '0',
commitmentMinTimeFraction: '0.5',
performanceHysteresisEpochs: 5678,
slaCompetitionFactor: '0',
} as unknown as LiquidityProvisionFields,
} as LiquidityProvisionFieldsFragment,
],
liquidityProviders: [
{
@@ -52,12 +49,9 @@ const input = {
const result = [
{
__typename: undefined,
balance: 1.8003328918633597e22,
earmarkedFees: 0,
balance: '1.8003328918633596575e+22',
commitmentAmount: '18003328918633596575000',
createdAt: '2022-12-16T09:28:29.071781Z',
commitmentMinTimeFraction: '0.5',
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
feeShare: {
equityLikeShare: '1',
__typename: 'LiquidityProviderFeeShare',
@@ -65,9 +59,6 @@ const result = [
},
fee: '0.001',
partyId: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
performanceHysteresisEpochs: 5678,
priceRange: '0',
slaCompetitionFactor: '0',
party: {
__typename: 'Party',
accountsConnection: {
@@ -115,9 +106,7 @@ describe('getLiquidityProvision', () => {
{
__typename: 'LiquidityProvision',
commitmentAmount: '18003328918633596575000',
commitmentMinTimeFraction: '0.5',
createdAt: '2022-12-16T09:28:29.071781Z',
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
fee: '0.001',
party: {
__typename: 'Party',
@@ -136,9 +125,6 @@ describe('getLiquidityProvision', () => {
},
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
},
performanceHysteresisEpochs: 5678,
priceRange: '0',
slaCompetitionFactor: '0',
status: 'STATUS_ACTIVE',
updatedAt: '2023-01-04T22:13:27.761985Z',
},
@@ -20,24 +20,20 @@ import type {
LiquidityProvisionsQueryVariables,
} from './__generated__/MarketLiquidity';
export type LiquidityProvisionFields = LiquidityProvisionFieldsFragment &
Schema.LiquiditySLAParameters;
export const liquidityProvisionsDataProvider = makeDataProvider<
LiquidityProvisionsQuery,
LiquidityProvisionFields[],
LiquidityProvisionFieldsFragment[],
never,
never,
LiquidityProvisionsQueryVariables
>({
query: LiquidityProvisionsDocument,
getData: (responseData: LiquidityProvisionsQuery | null) => {
return (responseData?.market?.liquidityProvisions?.edges
?.filter((n) => !!n)
.map((e) => ({
...e?.node.current,
...responseData.market?.liquiditySLAParameters,
})) ?? []) as LiquidityProvisionFields[];
return (
responseData?.market?.liquidityProvisionsConnection?.edges?.map(
(e) => e?.node
) ?? []
).filter((n) => !!n) as LiquidityProvisionFieldsFragment[];
},
});
@@ -85,7 +81,10 @@ export const lpAggregatedDataProvider = makeDerivedDataProvider<
}
);
export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
export const matchFilter = (
filter: Filter,
lp: LiquidityProvisionFieldsFragment
) => {
if (filter.partyId && lp.party.id !== filter.partyId) {
return false;
}
@@ -105,18 +104,15 @@ export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
};
export interface LiquidityProvisionData
extends Omit<LiquidityProvisionFieldsFragment, '__typename'>,
Partial<LiquidityProviderFieldsFragment>,
Omit<Schema.LiquiditySLAParameters, '__typename'> {
extends Omit<LiquidityProvisionFieldsFragment, '__typename'> {
assetDecimalPlaces?: number;
balance?: number;
balance?: string;
averageEntryValuation?: string;
equityLikeShare?: string;
earmarkedFees?: number;
}
export const getLiquidityProvision = (
liquidityProvisions: LiquidityProvisionFields[],
liquidityProvisions: LiquidityProvisionFieldsFragment[],
liquidityProvider: LiquidityProviderFieldsFragment[],
filter?: Filter
): LiquidityProvisionData[] => {
@@ -145,29 +141,17 @@ export const getLiquidityProvision = (
const bondAccounts = accounts?.filter(
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_BOND
);
const feeAccounts = accounts?.filter(
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES
);
const balance =
bondAccounts
?.reduce(
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
new BigNumber(0)
)
.toNumber() ?? 0;
const earmarkedFees =
feeAccounts
?.reduce(
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
new BigNumber(0)
)
.toNumber() ?? 0;
.toString() || '0';
return {
...lp,
...lpObj,
balance,
earmarkedFees,
__typename: undefined,
};
});
@@ -55,10 +55,9 @@ describe('LiquidityTable', () => {
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Fees accrued this epoch',
'Live time on book',
'Live time fraction on book',
'Live liquidity quality score (%)',
'Last time on the book',
'Last time fraction on the book',
'Last fee penalty',
'Last bond penalty',
'Status',
+4 -92
View File
@@ -96,58 +96,6 @@ export const LiquidityTable = ({
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
};
const feesAccruedTooltip = ({ value, data }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
let lessThanFull = false,
lessThanMinimum = false;
if (data.sla) {
lessThanFull =
data.sla &&
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
1
);
lessThanMinimum =
data.sla &&
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
data.commitmentMinTimeFraction
);
}
if (lessThanMinimum) {
return t(
`This LP's time on the book in the current epoch (%s) is less than the minimum required (%s), so they could lose all fee revenue for this epoch.`,
[
formatNumberPercentage(
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
2
),
formatNumberPercentage(
new BigNumber(data.commitmentMinTimeFraction).times(100),
2
),
]
);
}
if (lessThanFull) {
return t(
`This LP's time on the book in the current epoch (%s) is less than 100%, so they could lose some fees to a better performing LP.`,
[
formatNumberPercentage(
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
2
),
]
);
}
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
};
const stakeToCcyVolumeQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
@@ -230,7 +178,7 @@ export const LiquidityTable = ({
],
},
{
headerName: t('Live liquidity data'),
headerName: t('Live liquidity details'),
marryChildren: true,
children: [
{
@@ -244,41 +192,7 @@ export const LiquidityTable = ({
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Fees accrued this epoch'),
field: 'earmarkedFees',
type: 'rightAligned',
headerTooltip: t(
`The liquidity fees accrued by each provider, which will be distributed at the end of the epoch after applying any penalties.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: feesAccruedTooltip,
cellClassRules: {
'text-warning': ({ data }: { data: LiquidityProvisionData }) => {
if (!data.sla) return false;
return (
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(1) &&
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isGreaterThan(data.commitmentMinTimeFraction)
);
},
'text-red-500': ({ data }: { data: LiquidityProvisionData }) => {
if (!data.sla) return false;
return (
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(data.commitmentMinTimeFraction) &&
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isGreaterThan(0)
);
},
},
},
{
headerName: t(`Live time on book`),
headerName: t(`Live time fraction on book`),
field: 'sla.currentEpochFractionOfTimeOnBook',
type: 'rightAligned',
headerTooltip: t('Current epoch fraction of time on the book.'),
@@ -298,7 +212,7 @@ export const LiquidityTable = ({
marryChildren: true,
children: [
{
headerName: t(`Last time on the book`),
headerName: t(`Last time fraction on the book`),
field: 'sla.lastEpochFractionOfTimeOnBook',
type: 'rightAligned',
headerTooltip: t('Last epoch fraction of time on the book.'),
@@ -361,9 +275,7 @@ export const LiquidityTable = ({
return (
<AgGrid
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }: { data: LiquidityProvisionData }) => {
return data.id || '';
}}
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
tooltipShowDelay={500}
defaultColDef={defaultColDef}
{...props}
+4 -6
View File
@@ -12,14 +12,12 @@ export const liquidityProvisionsQuery = (
): LiquidityProvisionsQuery => {
const defaultResult: LiquidityProvisionsQuery = {
market: {
liquidityProvisions: {
__typename: 'LiquidityProvisionsWithPendingConnection',
liquidityProvisionsConnection: {
__typename: 'LiquidityProvisionsConnection',
edges: liquidityFields.map((node) => {
return {
__typename: 'LiquidityProvisionWithPendingEdge',
node: {
current: node,
},
__typename: 'LiquidityProvisionsEdge',
node,
};
}),
},
@@ -106,9 +106,10 @@ export const getLiquidityForMarket = (
markets: LiquidityProvisionMarket[]
) => {
const liquidity =
markets.find((m) => m.id === marketId)?.liquidityProvisions?.edges || [];
markets.find((m) => m.id === marketId)?.liquidityProvisionsConnection
?.edges || [];
return liquidity.map((l) => l?.node.current);
return liquidity.map((l) => l?.node);
};
export const getTargetStake = (
@@ -1001,7 +1001,7 @@ export const LiquiditySLAParametersInfoPanel = ({
const { params: networkParams } = useNetworkParams([
NetworkParams.market_liquidity_bondPenaltyParameter,
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltySlope,
NetworkParams.market_liquidity_nonPerformanceBondPenaltySlope,
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltyMax,
NetworkParams.market_liquidity_maximumLiquidityFeeFactorLevel,
NetworkParams.market_liquidity_stakeToCcyVolume,
@@ -1017,7 +1017,7 @@ export const LiquiditySLAParametersInfoPanel = ({
bondPenaltyParameter:
networkParams['market_liquidity_bondPenaltyParameter'],
nonPerformanceBondPenaltySlope:
networkParams['market_liquidity_sla_nonPerformanceBondPenaltySlope'],
networkParams['market_liquidity_nonPerformanceBondPenaltySlope'],
nonPerformanceBondPenaltyMax:
networkParams['market_liquidity_sla_nonPerformanceBondPenaltyMax'],
maxLiquidityFeeFactorLevel:
@@ -159,8 +159,8 @@ export const NetworkParams = {
'market_liquidity_targetstake_triggering_ratio',
market_liquidity_bondPenaltyParameter:
'market_liquidity_bondPenaltyParameter',
market_liquidity_sla_nonPerformanceBondPenaltySlope:
'market_liquidity_sla_nonPerformanceBondPenaltySlope',
market_liquidity_nonPerformanceBondPenaltySlope:
'market_liquidity_nonPerformanceBondPenaltySlope',
market_liquidity_sla_nonPerformanceBondPenaltyMax:
'market_liquidity_sla_nonPerformanceBondPenaltyMax',
market_liquidity_maximumLiquidityFeeFactorLevel:
@@ -166,7 +166,7 @@ export const ordersProvider = makeDataProvider<
pagination: {
getPageInfo,
append,
first: 1000,
first: 5000,
},
resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true },
@@ -27,7 +27,6 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(generateJsx());
@@ -46,7 +45,6 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(generateJsx());
@@ -64,7 +62,6 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(
@@ -1,6 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import { useCallback, useRef, useState, useEffect } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { FilterChangedEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -11,7 +12,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
import { Splash } from '@vegaprotocol/ui-toolkit';
export enum Filter {
'Open' = 'Open',
@@ -47,11 +48,33 @@ export const OrderListManager = ({
? { partyId, filter: { liveOnly: true } }
: { partyId };
const { data, error, pageInfo, load } = useDataProvider({
const { data, error } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables,
update: ({ data }) => {
if (data && gridRef.current?.api) {
gridRef.current.api.setRowData(data);
return true;
}
return false;
},
});
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
gridProps?.onFilterChanged?.(event);
if (event.api) {
const isEmpty = event.api.getDisplayedRowCount() === 0;
if (isEmpty) {
event.api.showNoRowsOverlay();
} else {
event.api.hideOverlay();
}
}
},
[gridProps]
);
useEffect(() => {
if (!data || !data.length) {
gridRef.current?.api?.showNoRowsOverlay();
@@ -73,17 +96,6 @@ export const OrderListManager = ({
[create]
);
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
if (error) {
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
}
@@ -91,60 +103,20 @@ export const OrderListManager = ({
return (
<>
<div className="h-full relative">
<div className="flex flex-col h-full">
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={(event) => {
onRowDataUpdated(event);
if (onFilterChanged) {
onFilterChanged(event);
}
}}
onRowDataUpdated={onRowDataUpdated}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{variables.filter?.liveOnly
? null
: t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
{data ? (
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button
size="extra-small"
className="ml-1"
onClick={() => load()}
>
{t('Load more')}
</Button>
) : null}
</div>
) : null}
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No orders matching selected filters')}
</div>
) : null}
</div>
</div>
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...gridProps}
onFilterChanged={onFilterChanged}
/>
</div>
{editOrder && (
<OrderEditDialog
@@ -30,7 +30,6 @@ describe('StopOrdersManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
await act(async () => {
render(generateJsx());
@@ -12,15 +12,10 @@ subscription ProposalEvent($partyId: ID!) {
}
}
fragment OnProposalFragment on Proposal {
fragment UpdateNetworkParameterProposal on Proposal {
id
state
datetime
rationale {
title
description
}
rejectionReason
terms {
enactmentDatetime
change {
@@ -31,9 +26,9 @@ fragment OnProposalFragment on Proposal {
}
}
subscription OnProposal {
subscription OnUpdateNetworkParameters {
proposals {
...OnProposalFragment
...UpdateNetworkParameterProposal
}
}
@@ -13,12 +13,12 @@ export type ProposalEventSubscriptionVariables = Types.Exact<{
export type ProposalEventSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } };
export type OnProposalFragmentFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateNetworkParameterProposalFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type OnProposalSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
export type OnUpdateNetworkParametersSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
export type OnProposalSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
export type OnUpdateNetworkParametersSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
export type ProposalOfMarketQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
@@ -64,16 +64,11 @@ export const ProposalEventFieldsFragmentDoc = gql`
errorDetails
}
`;
export const OnProposalFragmentFragmentDoc = gql`
fragment OnProposalFragment on Proposal {
export const UpdateNetworkParameterProposalFragmentDoc = gql`
fragment UpdateNetworkParameterProposal on Proposal {
id
state
datetime
rationale {
title
description
}
rejectionReason
terms {
enactmentDatetime
change {
@@ -114,35 +109,35 @@ export function useProposalEventSubscription(baseOptions: Apollo.SubscriptionHoo
}
export type ProposalEventSubscriptionHookResult = ReturnType<typeof useProposalEventSubscription>;
export type ProposalEventSubscriptionResult = Apollo.SubscriptionResult<ProposalEventSubscription>;
export const OnProposalDocument = gql`
subscription OnProposal {
export const OnUpdateNetworkParametersDocument = gql`
subscription OnUpdateNetworkParameters {
proposals {
...OnProposalFragment
...UpdateNetworkParameterProposal
}
}
${OnProposalFragmentFragmentDoc}`;
${UpdateNetworkParameterProposalFragmentDoc}`;
/**
* __useOnProposalSubscription__
* __useOnUpdateNetworkParametersSubscription__
*
* To run a query within a React component, call `useOnProposalSubscription` and pass it any options that fit your needs.
* When your component renders, `useOnProposalSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* To run a query within a React component, call `useOnUpdateNetworkParametersSubscription` and pass it any options that fit your needs.
* When your component renders, `useOnUpdateNetworkParametersSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useOnProposalSubscription({
* const { data, loading, error } = useOnUpdateNetworkParametersSubscription({
* variables: {
* },
* });
*/
export function useOnProposalSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnProposalSubscription, OnProposalSubscriptionVariables>) {
export function useOnUpdateNetworkParametersSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<OnProposalSubscription, OnProposalSubscriptionVariables>(OnProposalDocument, options);
return Apollo.useSubscription<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>(OnUpdateNetworkParametersDocument, options);
}
export type OnProposalSubscriptionHookResult = ReturnType<typeof useOnProposalSubscription>;
export type OnProposalSubscriptionResult = Apollo.SubscriptionResult<OnProposalSubscription>;
export type OnUpdateNetworkParametersSubscriptionHookResult = ReturnType<typeof useOnUpdateNetworkParametersSubscription>;
export type OnUpdateNetworkParametersSubscriptionResult = Apollo.SubscriptionResult<OnUpdateNetworkParametersSubscription>;
export const ProposalOfMarketDocument = gql`
query ProposalOfMarket($marketId: ID!) {
proposal(id: $marketId) {
@@ -3,7 +3,7 @@ export * from './use-proposal-event';
export * from './use-vega-transaction';
export * from './use-proposal-submit';
export * from './use-update-proposal';
export * from './use-proposal-toasts';
export * from './use-update-network-paramaters-toasts';
export * from './use-successor-market-proposal-details';
export * from './use-new-transfer-proposal-details';
export * from './use-cancel-transfer-proposal-details';
@@ -1,273 +0,0 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { ProposalRejectionReason } from '@vegaprotocol/types';
import {
ProposalChangeMapping,
ProposalState,
ProposalStateMapping,
} from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import {
PROPOSAL_STATES_TO_TOAST,
ProposalToastContent,
useProposalToasts,
} from './use-proposal-toasts';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { waitFor, renderHook, render } from '@testing-library/react';
import {
OnProposalDocument,
type OnProposalFragmentFragment,
type OnProposalSubscription,
} from './__generated__/Proposal';
import sample from 'lodash/sample';
const renderUseProposalToasts = (mocks?: MockedResponse[]) => {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
return renderHook(() => useProposalToasts(), { wrapper });
};
type ProposalChange = OnProposalFragmentFragment['terms']['change'];
const NEW_MARKET_CHANGE: ProposalChange = { __typename: 'NewMarket' };
const UPDATE_MARKET_CHANGE: ProposalChange = { __typename: 'UpdateMarket' };
const UPDATE_NETWORK_PARAMETER_CHANGE: ProposalChange = {
__typename: 'UpdateNetworkParameter',
networkParameter: {
__typename: 'NetworkParameter',
key: 'abc.def',
value: '123',
},
};
const NEW_ASSET_CHANGE: ProposalChange = { __typename: 'NewAsset' };
const UPDATE_ASSET_CHANGE: ProposalChange = { __typename: 'UpdateAsset' };
const NEW_FREEFORM_CHANGE: ProposalChange = { __typename: 'NewFreeform' };
const NEW_TRANSFER_CHANGE: ProposalChange = { __typename: 'NewTransfer' };
const CANCEL_TRANSFER_CHANGE: ProposalChange = { __typename: 'CancelTransfer' };
const UPDATE_MARKET_STATE_CHANGE: ProposalChange = {
__typename: 'UpdateMarketState',
};
const NEW_SPOT_MARKET_CHANGE: ProposalChange = { __typename: 'NewSpotMarket' };
const UPDATE_SPOT_MARKET_CHANGE: ProposalChange = {
__typename: 'UpdateSpotMarket',
};
const UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE: ProposalChange = {
__typename: 'UpdateVolumeDiscountProgram',
};
const UPDATE_REFERRAL_PROGRAM_CHANGE: ProposalChange = {
__typename: 'UpdateReferralProgram',
};
const GenericToastProposals = [
NEW_MARKET_CHANGE,
UPDATE_MARKET_CHANGE,
NEW_ASSET_CHANGE,
UPDATE_ASSET_CHANGE,
NEW_FREEFORM_CHANGE,
NEW_TRANSFER_CHANGE,
CANCEL_TRANSFER_CHANGE,
UPDATE_MARKET_STATE_CHANGE,
NEW_SPOT_MARKET_CHANGE,
UPDATE_SPOT_MARKET_CHANGE,
UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE,
UPDATE_REFERRAL_PROGRAM_CHANGE,
];
const generateProposal = (
title: string,
state: ProposalState = ProposalState.STATE_OPEN,
change: ProposalChange = { __typename: undefined },
rejectionReason: ProposalRejectionReason | null = null
): OnProposalFragmentFragment => ({
__typename: 'Proposal',
id: Math.random().toString(),
datetime: Math.random().toString(),
rationale: {
title,
description: '',
},
rejectionReason,
state,
terms: {
__typename: 'ProposalTerms',
enactmentDatetime: '2022-12-09T14:40:38Z',
change,
},
});
const INITIAL = useToasts.getState();
const clear = () => {
useToasts.setState(INITIAL);
};
describe('useProposalToasts', () => {
beforeEach(clear);
afterAll(clear);
it.each(PROPOSAL_STATES_TO_TOAST)(
'renders toast for %s proposal',
async (state) => {
const mockProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: generateProposal(
'Things to change',
state,
NEW_MARKET_CHANGE
),
},
},
};
const { result } = renderUseProposalToasts([mockProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(1);
});
}
);
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
}) as ProposalState[];
it.each(IGNORE_STATES)(
'does not render toast for %s proposal',
async (state) => {
const mockFailedProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: generateProposal('Things to change but ignored', state),
},
},
};
const { result } = renderUseProposalToasts([mockFailedProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
}
);
it('does not render toast for empty proposal', async () => {
const error = console.error;
console.error = () => {
/* no op */
};
const mockEmptyProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: undefined as unknown as OnProposalFragmentFragment,
},
},
};
const { result } = renderUseProposalToasts([mockEmptyProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
console.error = error;
});
const allTypes: [
ProposalChange['__typename'],
ProposalChange,
ProposalState?
][] = [...GenericToastProposals, UPDATE_NETWORK_PARAMETER_CHANGE].map(
(ch) => [ch.__typename, ch, sample(PROPOSAL_STATES_TO_TOAST)]
);
it.each(allTypes)(
'renders toast for %s proposal',
async (_, change, state) => {
const proposalData = generateProposal('Things to change', state, change);
const mockProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: proposalData,
},
},
};
const { result } = renderUseProposalToasts([mockProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(1);
});
}
);
});
describe('ProposalToastContent', () => {
const genericTypes: [
ProposalChange['__typename'],
ProposalChange,
ProposalState?
][] = GenericToastProposals.map((ch) => [
ch.__typename,
ch,
sample(PROPOSAL_STATES_TO_TOAST),
]);
it.each(genericTypes)(
'renders generic toast content for %s',
async (_, change, state) => {
const proposalData = generateProposal('Things to change', state, change);
const { container } = render(
<ProposalToastContent proposal={proposalData} />
);
const title = container.querySelector(
'[data-testid="proposal-toast-title"]'
);
const rationale = container.querySelector(
'[data-testid="proposal-toast-rationale-title"]'
);
const expectedChangeName = change.__typename
? ProposalChangeMapping[change.__typename]
: '';
const expectedState =
ProposalStateMapping[proposalData.state].toLocaleLowerCase();
expect(title).toHaveTextContent(
`${expectedChangeName} proposal ${expectedState}`
);
expect(rationale).toHaveTextContent('Things to change');
}
);
it('renders specific content for UpdateNetworkParameter proposal', () => {
const proposalData = generateProposal(
'Things to change',
ProposalState.STATE_OPEN,
UPDATE_NETWORK_PARAMETER_CHANGE
);
const { container } = render(
<ProposalToastContent proposal={proposalData} />
);
const title = container.querySelector(
'[data-testid="proposal-toast-title"]'
);
const rationale = container.querySelector(
'[data-testid="proposal-toast-rationale-title"]'
);
const param = container.querySelector(
'[data-testid="proposal-toast-network-param"]'
);
expect(title).toHaveTextContent('Update network parameter proposal open');
expect(rationale).toBe(null);
expect(param).toHaveTextContent('Update abc.def to 123');
});
});
@@ -1,136 +0,0 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
ProposalChangeMapping,
ProposalRejectionReasonMapping,
ProposalStateMapping,
} from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import { useCallback } from 'react';
import {
useOnProposalSubscription,
type OnProposalFragmentFragment,
} from './__generated__/Proposal';
export const PROPOSAL_STATES_TO_TOAST = [
ProposalState.STATE_DECLINED,
ProposalState.STATE_ENACTED,
ProposalState.STATE_OPEN,
ProposalState.STATE_PASSED,
];
const CLOSE_AFTER = 0;
type Proposal = OnProposalFragmentFragment;
const ProposalDetails = ({ proposal }: { proposal: Proposal }) => {
const change = proposal.terms.change;
switch (change.__typename) {
case 'UpdateNetworkParameter':
return <UpdateNetworkParameterDetails proposal={proposal} />;
default:
// generic details: rationale title and rejection reason if rejected
return (
<>
{proposal.rationale.title ? (
<p data-testid="proposal-toast-rationale-title" className="italic">
{proposal.rationale.title}
</p>
) : null}
{proposal.state === ProposalState.STATE_REJECTED &&
proposal.rejectionReason ? (
<p data-testid="proposal-toast-rejection-reason">
{t('Rejection reason:')}{' '}
{ProposalRejectionReasonMapping[proposal.rejectionReason]}
</p>
) : null}
</>
);
}
};
const UpdateNetworkParameterDetails = ({
proposal,
}: {
proposal: Proposal;
}) => {
const change = proposal.terms.change;
if (change.__typename !== 'UpdateNetworkParameter') return null;
return (
<p data-testid="proposal-toast-network-param" className="italic">
'{t('Update ')}
<span className="break-all">{change.networkParameter.key}</span>
{t(' to ')}
<span>{change.networkParameter.value}</span>'
</p>
);
};
export const ProposalToastContent = ({ proposal }: { proposal: Proposal }) => {
const tokenLink = useLinks(DApp.Governance);
const change = proposal.terms.change;
// Generates toast's title,
// e.g. Update market proposal enacted, New transfer proposal open, ...
const title = t('%s proposal %s', [
change.__typename ? ProposalChangeMapping[change.__typename] : 'Unknown',
ProposalStateMapping[proposal.state].toLowerCase(),
]);
const enactment = Date.parse(proposal.terms.enactmentDatetime);
return (
<div>
<ToastHeading data-testid="proposal-toast-title">{title}</ToastHeading>
<ProposalDetails proposal={proposal} />
{!isNaN(enactment) && (
<p>
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
</p>
)}
<p>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
>
{t('View proposal details')}
</ExternalLink>
</p>
</div>
);
};
export const useProposalToasts = () => {
const { setToast, remove } = useToasts((store) => ({
setToast: store.setToast,
remove: store.remove,
}));
const fromProposal = useCallback(
(proposal: Proposal): Toast => {
const id = `proposal-toast-${proposal.id}`;
return {
id,
intent: Intent.Warning,
content: <ProposalToastContent proposal={proposal} />,
onClose: () => {
remove(id);
},
closeAfter: CLOSE_AFTER,
};
},
[remove]
);
return useOnProposalSubscription({
onData: ({ data }) => {
const proposal = data.data?.proposals;
if (!proposal || !proposal.terms.change.__typename) return;
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
setToast(fromProposal(proposal));
}
},
});
};
@@ -0,0 +1,96 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { UpdateNetworkParameter } from '@vegaprotocol/types';
import { ProposalStateMapping } from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import { useCallback } from 'react';
import type { UpdateNetworkParameterProposalFragment } from './__generated__/Proposal';
import { useOnUpdateNetworkParametersSubscription } from './__generated__/Proposal';
export const PROPOSAL_STATES_TO_TOAST = [
ProposalState.STATE_DECLINED,
ProposalState.STATE_ENACTED,
ProposalState.STATE_OPEN,
ProposalState.STATE_PASSED,
];
const CLOSE_AFTER = 0;
type Proposal = UpdateNetworkParameterProposalFragment;
const UpdateNetworkParameterToastContent = ({
proposal,
}: {
proposal: Proposal;
}) => {
const tokenLink = useLinks(DApp.Governance);
const change = proposal.terms.change as UpdateNetworkParameter;
const title = t('Network change proposal %s').replace(
'%s',
ProposalStateMapping[proposal.state].toLowerCase()
);
const enactment = Date.parse(proposal.terms.enactmentDatetime);
return (
<div>
<ToastHeading>{title}</ToastHeading>
<p className="italic">
'{t('Update ')}
<span className="break-all">{change.networkParameter.key}</span>
{t(' to ')}
<span>{change.networkParameter.value}</span>'
</p>
{!isNaN(enactment) && (
<p>
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
</p>
)}
<p>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
>
{t('View proposal details')}
</ExternalLink>
</p>
</div>
);
};
export const useUpdateNetworkParametersToasts = () => {
const { setToast, remove } = useToasts((store) => ({
setToast: store.setToast,
remove: store.remove,
}));
const fromProposal = useCallback(
(proposal: Proposal): Toast => {
const id = `update-network-param-proposal-${proposal.id}`;
return {
id: `update-network-param-proposal-${proposal.id}`,
intent: Intent.Warning,
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
onClose: () => {
remove(id);
},
closeAfter: CLOSE_AFTER,
};
},
[remove]
);
return useOnUpdateNetworkParametersSubscription({
onData: ({ data }) => {
// note proposals is poorly named, it is actually a single proposal
const proposal = data.data?.proposals;
if (!proposal) return;
if (proposal.terms.change.__typename !== 'UpdateNetworkParameter') return;
// if one of the following states show a toast
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
setToast(fromProposal(proposal));
}
},
});
};
@@ -0,0 +1,168 @@
import merge from 'lodash/merge';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import {
PROPOSAL_STATES_TO_TOAST,
useUpdateNetworkParametersToasts,
} from './use-update-network-paramaters-toasts';
import type {
UpdateNetworkParameterProposalFragment,
OnUpdateNetworkParametersSubscription,
} from './__generated__/Proposal';
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { waitFor, renderHook } from '@testing-library/react';
const render = (mocks?: MockedResponse[]) => {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
return renderHook(() => useUpdateNetworkParametersToasts(), { wrapper });
};
const generateUpdateNetworkParametersProposal = (
key: string,
value: string,
state: ProposalState = ProposalState.STATE_OPEN
): UpdateNetworkParameterProposalFragment => ({
__typename: 'Proposal',
id: Math.random().toString(),
datetime: Math.random().toString(),
state,
terms: {
__typename: 'ProposalTerms',
enactmentDatetime: '2022-12-09T14:40:38Z',
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
__typename: 'NetworkParameter',
key,
value,
},
},
},
});
const INITIAL = useToasts.getState();
const clear = () => {
useToasts.setState(INITIAL);
};
describe('useUpdateNetworkParametersToasts', () => {
beforeEach(clear);
afterAll(clear);
it.each(PROPOSAL_STATES_TO_TOAST)(
'toasts for %s network param proposals',
async (state) => {
const mockOpenProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals: generateUpdateNetworkParametersProposal(
'abc.def',
'123.456',
state
),
},
},
};
const { result } = render([mockOpenProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(1);
});
}
);
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
}) as ProposalState[];
it.each(IGNORE_STATES)('does not toast for %s proposals', async (state) => {
const mockFailedProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals: generateUpdateNetworkParametersProposal(
'abc.def',
'123.456',
state
),
},
},
};
const { result } = render([mockFailedProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
});
it('does not return toast for empty propsal', async () => {
const error = console.error;
console.error = () => {
/* no op */
};
const mockEmptyProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals:
undefined as unknown as UpdateNetworkParameterProposalFragment,
},
},
};
const { result } = render([mockEmptyProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
console.error = error;
});
it('does not return toast for wrong proposal type', async () => {
const wrongProposalType = merge(
generateUpdateNetworkParametersProposal('a', 'b'),
{
terms: {
change: {
__typename: 'NewMarket',
},
},
}
);
const mockWrongProposalType: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals: wrongProposalType,
},
},
};
const { result } = render([mockWrongProposalType]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
});
});
+10 -54
View File
@@ -4,13 +4,10 @@ import { TradesTable } from './trades-table';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
import { t } from '@vegaprotocol/i18n';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useCallback, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface TradesContainerProps {
marketId: string;
gridProps: ReturnType<typeof useDataGridEvents>;
gridProps?: ReturnType<typeof useDataGridEvents>;
}
export const TradesManager = ({
@@ -19,60 +16,19 @@ export const TradesManager = ({
}: TradesContainerProps) => {
const update = useDealTicketFormValues((state) => state.updateAll);
const { data, error, load, pageInfo } = useDataProvider({
const { data, error } = useDataProvider({
dataProvider: tradesWithMarketProvider,
variables: { marketId },
});
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
return (
<div className="flex flex-col h-full">
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
onFilterChanged={(event) => {
onRowDataUpdated(event);
onFilterChanged(event);
}}
onRowDataUpdated={onRowDataUpdated}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No trades matching selected filters')}
</div>
) : null}
</div>
</div>
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...gridProps}
/>
);
};
+3 -42
View File
@@ -813,7 +813,7 @@ export type DispatchStrategy = {
/** Minimum notional time-weighted averaged position required for a party to be considered eligible */
notionalTimeWeightedAveragePositionRequirement: Scalars['String'];
/** Ascending order list of start rank and corresponding share ratio */
rankTable?: Maybe<Array<Maybe<RankTable>>>;
rankTable?: Maybe<RankTable>;
/** Minimum number of governance tokens, e.g. VEGA, staked for a party to be considered eligible */
stakingRequirement: Scalars['String'];
/** The teams in scope for the reward, if the entity is teams */
@@ -3387,8 +3387,6 @@ export type Party = {
transfersConnection?: Maybe<TransferConnection>;
/** The current reward vesting summary of the party for the last epoch */
vestingBalancesSummary: PartyVestingBalancesSummary;
/** The current statistics about a party's vesting rewards for the last epoch */
vestingStats?: Maybe<PartyVestingStats>;
/** All votes on proposals in the Vega network by the given party */
votesConnection?: Maybe<ProposalVoteConnection>;
/** The list of all withdrawals initiated by the party */
@@ -3504,7 +3502,6 @@ export type PartytradesConnectionArgs = {
/** Represents a party on Vega, could be an ethereum wallet address in the future */
export type PartytransfersConnectionArgs = {
direction?: InputMaybe<TransferDirection>;
isReward?: InputMaybe<Scalars['Boolean']>;
pagination?: InputMaybe<Pagination>;
};
@@ -3619,17 +3616,6 @@ export type PartyVestingBalancesSummary = {
vestingBalances?: Maybe<Array<PartyVestingBalance>>;
};
/** Statistics about a party's vesting rewards */
export type PartyVestingStats = {
__typename?: 'PartyVestingStats';
/** Epoch for which the statistics are valid */
epochSeq: Scalars['Int'];
/** The balance of the party, in quantum. */
quantumBalance: Scalars['String'];
/** The reward bonus multiplier */
rewardBonusMultiplier: Scalars['String'];
};
/** Create an order linked to an index rather than a price */
export type PeggedOrder = {
__typename?: 'PeggedOrder';
@@ -4833,7 +4819,7 @@ export type QueryprotocolUpgradeProposalsArgs = {
/** Queries allow a caller to read data and filter data via GraphQL. */
export type QueryreferralSetRefereesArgs = {
aggregationEpochs?: InputMaybe<Scalars['Int']>;
aggregationDays?: InputMaybe<Scalars['Int']>;
id?: InputMaybe<Scalars['ID']>;
pagination?: InputMaybe<Pagination>;
referee?: InputMaybe<Scalars['ID']>;
@@ -4919,7 +4905,6 @@ export type QuerytransferArgs = {
/** Queries allow a caller to read data and filter data via GraphQL. */
export type QuerytransfersConnectionArgs = {
direction?: InputMaybe<TransferDirection>;
isReward?: InputMaybe<Scalars['Boolean']>;
pagination?: InputMaybe<Pagination>;
partyId?: InputMaybe<Scalars['ID']>;
};
@@ -5090,16 +5075,12 @@ export type ReferralSetStats = {
partyId: Scalars['ID'];
/** Running volume for the set based on the window length of the current referral program. */
referralSetRunningNotionalTakerVolume: Scalars['String'];
/** The referrer's taker volume */
referrerTakerVolume: Scalars['String'];
/** Reward factor applied to the party. */
rewardFactor: Scalars['String'];
/** The proportion of the referees taker fees to be rewarded to the referrer. */
rewardsFactorMultiplier: Scalars['String'];
/** The multiplier applied to the referral reward factor when calculating referral rewards due to the referrer. */
rewardsMultiplier: Scalars['String'];
/** Indicates if the referral set was eligible to be part of the referral program. */
wasEligible: Scalars['Boolean'];
};
/** Connection type for retrieving cursor-based paginated referral set statistics information */
@@ -6121,31 +6102,11 @@ export enum TransferDirection {
export type TransferEdge = {
__typename?: 'TransferEdge';
cursor: Scalars['String'];
node: TransferNode;
};
/** A transfer fee record */
export type TransferFee = {
__typename?: 'TransferFee';
/** The fee amount */
amount: Scalars['String'];
/** The epoch when this fee was paid */
epoch: Scalars['Int'];
/** Transfer ID of the transfer for which the fee was paid */
transferId: Scalars['ID'];
node: Transfer;
};
export type TransferKind = OneOffGovernanceTransfer | OneOffTransfer | RecurringGovernanceTransfer | RecurringTransfer;
/** A transfer record with the fee payments associated with the transfer */
export type TransferNode = {
__typename?: 'TransferNode';
/** The list of fee payments made */
fees?: Maybe<Array<Maybe<TransferFee>>>;
/** The transfer record */
transfer: Transfer;
};
export type TransferResponse = {
__typename?: 'TransferResponse';
/** The balances of accounts involved in the transfer */
-24
View File
@@ -3,7 +3,6 @@ import type {
GovernanceTransferKind,
GovernanceTransferType,
PeggedReference,
ProposalChange,
} from './__generated__/types';
import type { AccountType } from './__generated__/types';
import type {
@@ -300,29 +299,6 @@ export const OrderTypeMapping: {
TYPE_NETWORK: 'Network',
};
/**
* Proposal change type mapping
*/
export const ProposalChangeMapping: Record<
NonNullable<ProposalChange['__typename']>,
string
> = {
NewMarket: 'New market',
UpdateMarket: 'Update market',
UpdateNetworkParameter: 'Update network parameter',
NewAsset: 'New asset',
UpdateAsset: 'Update asset',
/* cspell:disable-next-line */
NewFreeform: 'New free-form',
NewTransfer: 'New transfer',
CancelTransfer: 'Cancel transfer',
UpdateMarketState: 'Update market state',
NewSpotMarket: 'New spot market',
UpdateSpotMarket: 'Update spot market',
UpdateVolumeDiscountProgram: 'Update volume discount program',
UpdateReferralProgram: 'Update referral program',
};
/**
* Reason for the proposal being rejected by the core node
*/